Download all related files using their existing name by default #449
Related issues: - Configure download name for files and albums #848 - When downloading live photos, zip all the associated files #437
This commit is contained in:
parent
40ccb29313
commit
993e7466fa
19 changed files with 199 additions and 123 deletions
|
@ -174,14 +174,14 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
this.onDownload(`/api/v1/albums/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
|
||||
|
||||
this.expanded = false;
|
||||
},
|
||||
onDownload(path) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
download(path, "album.zip");
|
||||
download(path, "photoprism-album.zip");
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -157,6 +157,7 @@ import Api from "common/api";
|
|||
import Notify from "common/notify";
|
||||
import Event from "pubsub-js";
|
||||
import download from "common/download";
|
||||
import Photo from "model/photo";
|
||||
|
||||
export default {
|
||||
name: 'PPhotoClipboard',
|
||||
|
@ -258,19 +259,19 @@ export default {
|
|||
this.clearClipboard();
|
||||
},
|
||||
download() {
|
||||
if (this.selection.length === 1) {
|
||||
this.onDownload(`/api/v1/photos/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
|
||||
} else {
|
||||
Api.post("zip", {"photos": this.selection}).then(r => {
|
||||
switch (this.selection.length) {
|
||||
case 0: return;
|
||||
case 1: new Photo().find(this.selection[0]).then(p => p.downloadAll()); break;
|
||||
default: Api.post("zip", {"photos": this.selection}).then(r => {
|
||||
this.onDownload(`/api/v1/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
|
||||
});
|
||||
}
|
||||
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
this.expanded = false;
|
||||
},
|
||||
onDownload(path) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
download(path, "photos.zip");
|
||||
},
|
||||
edit() {
|
||||
|
|
|
@ -181,10 +181,8 @@ export default {
|
|||
}
|
||||
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
let photo = new Photo();
|
||||
photo.find(this.item.uid).then((p) => {
|
||||
p.downloadAll();
|
||||
});
|
||||
|
||||
new Photo().find(this.item.uid).then(p => p.downloadAll());
|
||||
},
|
||||
onEdit() {
|
||||
this.onPause();
|
||||
|
|
|
@ -416,21 +416,28 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
downloadAll() {
|
||||
const token = config.downloadToken();
|
||||
|
||||
if (!this.Files) {
|
||||
download(
|
||||
`/api/v1/dl/${this.mainFileHash()}?t=${config.downloadToken()}`,
|
||||
this.baseName(false)
|
||||
);
|
||||
const hash = this.mainFileHash();
|
||||
|
||||
if (hash) {
|
||||
download(`/api/v1/dl/${hash}?t=${token}`, this.baseName(false));
|
||||
} else if (config.debug) {
|
||||
console.log("download: failed, empty file hash", this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.Files.forEach((file) => {
|
||||
if (!file || !file.Hash) {
|
||||
console.warn("no file hash found for download", file);
|
||||
if (!file || !file.Hash || file.Sidecar) {
|
||||
// Don't download broken files and sidecars.
|
||||
if (config.debug) console.log("download: skipped file", file);
|
||||
return;
|
||||
}
|
||||
|
||||
download(`/api/v1/dl/${file.Hash}?t=${config.downloadToken()}`, this.fileBase(file.Name));
|
||||
download(`/api/v1/dl/${file.Hash}?t=${token}`, this.fileBase(file.Name));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -80,14 +80,14 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
this.onDownload(`/api/v1/albums/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
|
||||
|
||||
this.expanded = false;
|
||||
},
|
||||
onDownload(path) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
download(path, "album.zip");
|
||||
download(path, "photoprism-album.zip");
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
transition="slide-y-reverse-transition"
|
||||
class="p-clipboard p-photo-clipboard"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<template #activator>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
|
@ -49,6 +49,7 @@
|
|||
import Api from "common/api";
|
||||
import Notify from "common/notify";
|
||||
import download from "common/download";
|
||||
import Photo from "model/photo";
|
||||
|
||||
export default {
|
||||
name: 'PPhotoClipboard',
|
||||
|
@ -75,19 +76,19 @@ export default {
|
|||
this.expanded = false;
|
||||
},
|
||||
download() {
|
||||
if (this.selection.length === 1) {
|
||||
this.onDownload(`/api/v1/photos/${this.selection[0]}/dl?t=${this.$config.downloadToken()}`);
|
||||
} else {
|
||||
Api.post("zip", {"photos": this.selection}).then(r => {
|
||||
switch (this.selection.length) {
|
||||
case 0: return;
|
||||
case 1: new Photo().find(this.selection[0]).then(p => p.downloadAll()); break;
|
||||
default: Api.post("zip", {"photos": this.selection}).then(r => {
|
||||
this.onDownload(`/api/v1/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
|
||||
});
|
||||
}
|
||||
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
this.expanded = false;
|
||||
},
|
||||
onDownload(path) {
|
||||
Notify.success(this.$gettext("Downloading…"));
|
||||
|
||||
download(path, "photos.zip");
|
||||
},
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -188,10 +189,19 @@ func ShareWithAccount(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
var aliases = make(map[string]int)
|
||||
|
||||
for _, file := range files {
|
||||
dstFileName := path.Join(dst, file.ShareBase())
|
||||
fileShare := entity.NewFileShare(file.ID, m.ID, dstFileName)
|
||||
entity.FirstOrCreateFileShare(fileShare)
|
||||
alias := path.Join(dst, file.ShareBase(0))
|
||||
key := strings.ToLower(alias)
|
||||
|
||||
if seq := aliases[key]; seq > 0 {
|
||||
alias = file.ShareBase(seq)
|
||||
}
|
||||
|
||||
aliases[key] += 1
|
||||
|
||||
entity.FirstOrCreateFileShare(entity.NewFileShare(file.ID, m.ID, alias))
|
||||
}
|
||||
|
||||
workers.StartShare(service.Config())
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
@ -17,10 +19,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
@ -473,37 +471,61 @@ func DownloadAlbum(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
p, err := query.AlbumPhotos(a, 10000)
|
||||
files, err := query.AlbumPhotos(a, 10000)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
zipToken := rnd.Token(3)
|
||||
zipFileName := fmt.Sprintf("%s-%s.zip", strings.Title(a.AlbumSlug), zipToken)
|
||||
albumName := strings.Title(a.AlbumSlug)
|
||||
|
||||
if len(albumName) < 2 {
|
||||
albumName = fmt.Sprintf("photoprism-album-%s", a.AlbumUID)
|
||||
}
|
||||
|
||||
zipFileName := fmt.Sprintf("%s.zip", albumName)
|
||||
|
||||
AddDownloadHeader(c, zipFileName)
|
||||
|
||||
zipWriter := zip.NewWriter(c.Writer)
|
||||
defer func() { _ = zipWriter.Close() }()
|
||||
|
||||
for _, f := range p {
|
||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
fileAlias := f.ShareFileName()
|
||||
var aliases = make(map[string]int)
|
||||
|
||||
for _, file := range files {
|
||||
if file.FileHash == "" {
|
||||
log.Warnf("download: empty file hash, skipped %s", txt.Quote(file.FileName))
|
||||
continue
|
||||
}
|
||||
|
||||
if file.FileSidecar {
|
||||
log.Debugf("download: skipped sidecar %s", txt.Quote(file.FileName))
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := photoprism.FileName(file.FileRoot, file.FileName)
|
||||
alias := file.ShareBase(0)
|
||||
key := strings.ToLower(alias)
|
||||
|
||||
if seq := aliases[key]; seq > 0 {
|
||||
alias = file.ShareBase(seq)
|
||||
}
|
||||
|
||||
aliases[key] += 1
|
||||
|
||||
if fs.FileExists(fileName) {
|
||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||
if err := addFileToZip(zipWriter, fileName, alias); err != nil {
|
||||
log.Error(err)
|
||||
Abort(c, http.StatusInternalServerError, i18n.ErrZipFailed)
|
||||
return
|
||||
}
|
||||
log.Infof("album: added %s as %s", txt.Quote(f.FileName), txt.Quote(fileAlias))
|
||||
log.Infof("download: added %s as %s", txt.Quote(file.FileName), txt.Quote(alias))
|
||||
} else {
|
||||
log.Errorf("album: file %s is missing", txt.Quote(f.FileName))
|
||||
log.Errorf("download: file %s is missing", txt.Quote(file.FileName))
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("album: archive %s created in %s", txt.Quote(zipFileName), time.Since(start))
|
||||
log.Infof("download: album zip %s created in %s", txt.Quote(zipFileName), time.Since(start))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -117,13 +117,13 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase()})
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase(0)})
|
||||
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
AddCoverCacheHeader(c)
|
||||
|
||||
if c.Query("download") != "" {
|
||||
c.FileAttachment(thumbnail, f.ShareBase())
|
||||
c.FileAttachment(thumbnail, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(thumbnail)
|
||||
}
|
||||
|
@ -229,13 +229,13 @@ func LabelCover(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase()})
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase(0)})
|
||||
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
AddCoverCacheHeader(c)
|
||||
|
||||
if c.Query("download") != "" {
|
||||
c.FileAttachment(thumbnail, f.ShareBase())
|
||||
c.FileAttachment(thumbnail, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(thumbnail)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,20 @@ import (
|
|||
// TODO: GET /api/v1/dl/photo/:uid
|
||||
// TODO: GET /api/v1/dl/album/:uid
|
||||
|
||||
// DownloadName returns the download file name type.
|
||||
func DownloadName(c *gin.Context) entity.DownloadName {
|
||||
switch c.Query("name") {
|
||||
case "file":
|
||||
return entity.DownloadNameFile
|
||||
case "share":
|
||||
return entity.DownloadNameShare
|
||||
case "original":
|
||||
return entity.DownloadNameOriginal
|
||||
default:
|
||||
return service.Config().Settings().Download.Name
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/v1/dl/:hash
|
||||
//
|
||||
// Parameters:
|
||||
|
@ -51,30 +65,6 @@ func GetDownload(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
name := entity.DownloadNameFile
|
||||
|
||||
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.FileAttachment(fileName, downloadName)
|
||||
c.FileAttachment(fileName, f.DownloadName(DownloadName(c), 0))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -147,9 +147,7 @@ func GetPhotoDownload(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
downloadFileName := f.ShareBase()
|
||||
|
||||
c.FileAttachment(fileName, downloadFileName)
|
||||
c.FileAttachment(fileName, f.DownloadName(DownloadName(c), 0))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -154,13 +154,13 @@ func GetThumb(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase()})
|
||||
cache.SetDefault(cacheKey, ThumbCache{thumbnail, f.ShareBase(0)})
|
||||
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
||||
|
||||
AddThumbCacheHeader(c)
|
||||
|
||||
if download {
|
||||
c.FileAttachment(thumbnail, f.ShareBase())
|
||||
c.FileAttachment(thumbnail, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(thumbnail)
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
AddContentTypeHeader(c, ContentTypeAvc)
|
||||
|
||||
if c.Query("download") != "" {
|
||||
c.FileAttachment(fileName, f.ShareBase())
|
||||
c.FileAttachment(fileName, f.DownloadName(DownloadName(c), 0))
|
||||
} else {
|
||||
c.File(fileName)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,11 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
@ -17,7 +20,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -64,9 +66,8 @@ func CreateZip(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
zipPath := path.Join(conf.TempPath(), "zip")
|
||||
zipToken := rnd.Token(3)
|
||||
zipYear := time.Now().Format("January-2006")
|
||||
zipBaseName := fmt.Sprintf("Photos-%s-%s.zip", zipYear, zipToken)
|
||||
zipToken := rnd.Token(8)
|
||||
zipBaseName := fmt.Sprintf("photoprism-download-%s-%s.zip", time.Now().Format("20060102-150405"), zipToken)
|
||||
zipFileName := path.Join(zipPath, zipBaseName)
|
||||
|
||||
if err := os.MkdirAll(zipPath, 0700); err != nil {
|
||||
|
@ -86,25 +87,46 @@ func CreateZip(router *gin.RouterGroup) {
|
|||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, f := range files {
|
||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
fileAlias := f.ShareBase()
|
||||
dlName := DownloadName(c)
|
||||
|
||||
var aliases = make(map[string]int)
|
||||
|
||||
for _, file := range files {
|
||||
if file.FileHash == "" {
|
||||
log.Warnf("download: empty file hash, skipped %s", txt.Quote(file.FileName))
|
||||
continue
|
||||
}
|
||||
|
||||
if file.FileSidecar {
|
||||
log.Debugf("download: skipped sidecar %s", txt.Quote(file.FileName))
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := photoprism.FileName(file.FileRoot, file.FileName)
|
||||
alias := file.DownloadName(dlName, 0)
|
||||
key := strings.ToLower(alias)
|
||||
|
||||
if seq := aliases[key]; seq > 0 {
|
||||
alias = file.DownloadName(dlName, seq)
|
||||
}
|
||||
|
||||
aliases[key] += 1
|
||||
|
||||
if fs.FileExists(fileName) {
|
||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||
if err := addFileToZip(zipWriter, fileName, alias); err != nil {
|
||||
Error(c, http.StatusInternalServerError, err, i18n.ErrZipFailed)
|
||||
return
|
||||
}
|
||||
log.Infof("zip: added %s as %s", txt.Quote(f.FileName), txt.Quote(fileAlias))
|
||||
log.Infof("download: added %s as %s", txt.Quote(file.FileName), txt.Quote(alias))
|
||||
} else {
|
||||
log.Warnf("zip: file %s is missing", txt.Quote(f.FileName))
|
||||
logError("zip", f.Update("FileMissing", true))
|
||||
log.Warnf("download: file %s is missing", txt.Quote(file.FileName))
|
||||
logError("download", file.Update("FileMissing", true))
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := int(time.Since(start).Seconds())
|
||||
|
||||
log.Infof("zip: archive %s created in %s", txt.Quote(zipBaseName), time.Since(start))
|
||||
log.Infof("download: zip %s created in %s", txt.Quote(zipBaseName), time.Since(start))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgZipCreatedIn, elapsed), "filename": zipBaseName})
|
||||
})
|
||||
|
@ -132,7 +154,7 @@ func DownloadZip(router *gin.RouterGroup) {
|
|||
c.FileAttachment(zipFileName, zipBaseName)
|
||||
|
||||
if err := os.Remove(zipFileName); err != nil {
|
||||
log.Errorf("zip: failed removing %s (%s)", txt.Quote(zipFileName), err.Error())
|
||||
log.Errorf("download: failed removing %s (%s)", txt.Quote(zipFileName), err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -108,26 +108,50 @@ func (m *File) BeforeCreate(scope *gorm.Scope) error {
|
|||
return scope.SetColumn("FileUID", rnd.PPID('f'))
|
||||
}
|
||||
|
||||
// DownloadName returns the download file name.
|
||||
func (m *File) DownloadName(n DownloadName, seq int) string {
|
||||
switch n {
|
||||
case DownloadNameFile:
|
||||
return m.Base(seq)
|
||||
case DownloadNameOriginal:
|
||||
return m.OriginalBase(seq)
|
||||
default:
|
||||
return m.ShareBase(seq)
|
||||
}
|
||||
}
|
||||
|
||||
// Base returns the file name without path.
|
||||
func (m *File) Base() string {
|
||||
func (m *File) Base(seq int) string {
|
||||
if m.FileName == "" {
|
||||
return m.ShareBase()
|
||||
return m.ShareBase(seq)
|
||||
}
|
||||
|
||||
return filepath.Base(m.FileName)
|
||||
base := filepath.Base(m.FileName)
|
||||
|
||||
if seq > 0 {
|
||||
return fmt.Sprintf("%s (%d)%s", fs.StripExt(base), seq, filepath.Ext(base))
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
// OriginalBase returns the original file name without path.
|
||||
func (m *File) OriginalBase() string {
|
||||
func (m *File) OriginalBase(seq int) string {
|
||||
if m.OriginalName == "" {
|
||||
return m.Base()
|
||||
return m.Base(seq)
|
||||
}
|
||||
|
||||
return filepath.Base(m.OriginalName)
|
||||
base := filepath.Base(m.OriginalName)
|
||||
|
||||
if seq > 0 {
|
||||
return fmt.Sprintf("%s (%d)%s", fs.StripExt(base), seq, filepath.Ext(base))
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
// ShareBase returns a meaningful file name useful for sharing.
|
||||
func (m *File) ShareBase() string {
|
||||
// ShareBase returns a meaningful file name for sharing.
|
||||
func (m *File) ShareBase(seq int) string {
|
||||
photo := m.RelatedPhoto()
|
||||
|
||||
if photo == nil {
|
||||
|
@ -140,11 +164,12 @@ func (m *File) ShareBase() string {
|
|||
|
||||
name := strings.Title(slug.MakeLang(photo.PhotoTitle, "en"))
|
||||
taken := photo.TakenAtLocal.Format("20060102-150405")
|
||||
token := rnd.Token(3)
|
||||
|
||||
result := fmt.Sprintf("%s-%s-%s.%s", taken, name, token, m.FileType)
|
||||
if seq > 0 {
|
||||
return fmt.Sprintf("%s-%s (%d).%s", taken, name, seq, m.FileType)
|
||||
}
|
||||
|
||||
return result
|
||||
return fmt.Sprintf("%s-%s.%s", taken, name, m.FileType)
|
||||
}
|
||||
|
||||
// Changed returns true if new and old file size or modified time are different.
|
||||
|
|
|
@ -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.ShareBase()
|
||||
filename := file.ShareBase(0)
|
||||
|
||||
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.ShareBase()
|
||||
filename := file.ShareBase(0)
|
||||
|
||||
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.ShareBase()
|
||||
filename := file.ShareBase(0)
|
||||
|
||||
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.ShareBase()
|
||||
filename := file.ShareBase(0)
|
||||
|
||||
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.ShareBase()
|
||||
filename := file.ShareBase(0)
|
||||
|
||||
assert.Equal(t, filename, "e98ijhyt.jpg")
|
||||
})
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
|
@ -73,6 +72,7 @@ type PhotoResult struct {
|
|||
FileHeight int `json:"Height"`
|
||||
FilePortrait bool `json:"Portrait"`
|
||||
FilePrimary bool `json:"-"`
|
||||
FileSidecar bool `json:"-"`
|
||||
FileMissing bool `json:"-"`
|
||||
FileVideo bool `json:"-"`
|
||||
FileDuration time.Duration `json:"-"`
|
||||
|
@ -144,7 +144,8 @@ func (m PhotoResults) Merged() (PhotoResults, int, error) {
|
|||
return merged, count, nil
|
||||
}
|
||||
|
||||
func (m *PhotoResult) ShareFileName() string {
|
||||
// ShareBase returns a meaningful file name for sharing.
|
||||
func (m *PhotoResult) ShareBase(seq int) string {
|
||||
var name string
|
||||
|
||||
if m.PhotoTitle != "" {
|
||||
|
@ -154,9 +155,10 @@ func (m *PhotoResult) ShareFileName() string {
|
|||
}
|
||||
|
||||
taken := m.TakenAtLocal.Format("20060102-150405")
|
||||
token := rnd.Token(3)
|
||||
|
||||
result := fmt.Sprintf("%s-%s-%s.%s", taken, name, token, m.FileType)
|
||||
if seq > 0 {
|
||||
return fmt.Sprintf("%s-%s (%d).%s", taken, name, seq, m.FileType)
|
||||
}
|
||||
|
||||
return result
|
||||
return fmt.Sprintf("%s-%s.%s", taken, name, m.FileType)
|
||||
}
|
||||
|
|
|
@ -322,7 +322,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
|||
Files: nil,
|
||||
}
|
||||
|
||||
r := result1.ShareFileName()
|
||||
r := result1.ShareBase(0)
|
||||
assert.Contains(t, r, "20131111-090718-Phototitle123")
|
||||
})
|
||||
t.Run("without photo title", func(t *testing.T) {
|
||||
|
@ -385,7 +385,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
|||
Files: nil,
|
||||
}
|
||||
|
||||
r := result1.ShareFileName()
|
||||
r := result1.ShareBase(0)
|
||||
assert.Contains(t, r, "20151111-090718-uid123")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -27,11 +27,11 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
// Base query.
|
||||
s = s.Table("photos").
|
||||
Select(`photos.*, photos.id AS composite_id,
|
||||
files.id AS file_id, files.file_uid, files.instance_id, files.file_primary, files.file_missing, files.file_name,
|
||||
files.file_root, files.file_hash, files.file_codec, files.file_type, files.file_mime, files.file_width,
|
||||
files.file_height, files.file_portrait, files.file_aspect_ratio, files.file_orientation, files.file_main_color,
|
||||
files.file_colors, files.file_luminance, files.file_chroma, files.file_projection,
|
||||
files.file_diff, files.file_video, files.file_duration, files.file_size,
|
||||
files.id AS file_id, files.file_uid, files.instance_id, files.file_primary, files.file_sidecar,
|
||||
files.file_portrait,files.file_video, files.file_missing, files.file_name, files.file_root, files.file_hash,
|
||||
files.file_codec, files.file_type, files.file_mime, files.file_width, files.file_height,
|
||||
files.file_aspect_ratio, files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance,
|
||||
files.file_chroma, files.file_projection, files.file_diff, files.file_duration, files.file_size,
|
||||
cameras.camera_make, cameras.camera_model,
|
||||
lenses.lens_make, lenses.lens_model,
|
||||
places.place_label, places.place_city, places.place_state, places.place_country`).
|
||||
|
|
Loading…
Reference in a new issue