WebDAV: Upload of videos, RAWs, moments, months, and states #2293

This commit is contained in:
Michael Mayer
2022-05-16 23:59:28 +02:00
parent 87521c7e6d
commit 4f8e7c131a
7 changed files with 252 additions and 60 deletions

View File

@@ -110,3 +110,50 @@ func AlbumEntryFound(uid string) error {
return UnscopedDb().Exec(`UPDATE photos_albums SET missing = 0 WHERE photo_uid = ?`, uid).Error return UnscopedDb().Exec(`UPDATE photos_albums SET missing = 0 WHERE photo_uid = ?`, uid).Error
} }
} }
// AlbumsPhotoUIDs returns up to 10000 photo UIDs that belong to the specified albums.
func AlbumsPhotoUIDs(albums []string, includeDefault, includePrivate bool) (photos []string, err error) {
for _, albumUid := range albums {
a, err := AlbumByUID(albumUid)
if err != nil {
log.Warnf("query: album %s not found (%s)", albumUid, err.Error())
continue
}
if a.IsDefault() && !includeDefault {
continue
}
frm := form.SearchPhotos{
Album: a.AlbumUID,
Filter: a.AlbumFilter,
Count: 10000,
Offset: 0,
Public: !includePrivate,
Hidden: false,
Archived: false,
Quality: 1,
}
res, count, err := search.PhotoIds(frm)
if err != nil {
return photos, err
} else if count == 0 {
continue
}
ids := make([]string, 0, count)
for _, r := range res {
ids = append(ids, r.PhotoUID)
}
if len(ids) > 0 {
photos = append(photos, ids...)
}
}
return photos, nil
}

View File

@@ -46,10 +46,13 @@ func DownloadSelection(mediaRaw, mediaSidecar, originals bool) FileSelection {
} }
// ShareSelection selects files to share, for example for upload via WebDAV. // ShareSelection selects files to share, for example for upload via WebDAV.
func ShareSelection(primary bool) FileSelection { func ShareSelection(originals bool) FileSelection {
return FileSelection{ return FileSelection{
Originals: !primary, Originals: originals,
Primary: primary, Primary: !originals,
Hidden: false,
Private: false,
Archived: false,
MaxSize: 1024 * MegaByte, MaxSize: 1024 * MegaByte,
} }
} }
@@ -60,6 +63,13 @@ func SelectedFiles(f form.Selection, o FileSelection) (results entity.Files, err
return results, errors.New("no items selected") return results, errors.New("no items selected")
} }
// Resolve photos in smart albums.
if photoIds, err := AlbumsPhotoUIDs(f.Albums, false, o.Private); err != nil {
log.Warnf("query: %s", err.Error())
} else if len(photoIds) > 0 {
f.Photos = append(f.Photos, photoIds...)
}
var concat string var concat string
switch DbDialect() { switch DbDialect() {
case MySQL: case MySQL:
@@ -93,37 +103,37 @@ func SelectedFiles(f form.Selection, o FileSelection) (results entity.Files, err
// File size limit? // File size limit?
if o.MaxSize > 0 { if o.MaxSize > 0 {
s = s.Where("file_size < ?", o.MaxSize) s = s.Where("files.file_size < ?", o.MaxSize)
} }
// Specific media types only? // Specific media types only?
if len(o.Media) > 0 { if len(o.Media) > 0 {
s = s.Where("media_type IN (?)", o.Media) s = s.Where("files.media_type IN (?)", o.Media)
} }
// Exclude media types? // Exclude media types?
if len(o.OmitMedia) > 0 { if len(o.OmitMedia) > 0 {
s = s.Where("media_type NOT IN (?)", o.OmitMedia) s = s.Where("files.media_type NOT IN (?)", o.OmitMedia)
} }
// Specific file types only? // Specific file types only?
if len(o.Types) > 0 { if len(o.Types) > 0 {
s = s.Where("file_type IN (?)", o.Types) s = s.Where("files.file_type IN (?)", o.Types)
} }
// Exclude file types? // Exclude file types?
if len(o.OmitTypes) > 0 { if len(o.OmitTypes) > 0 {
s = s.Where("file_type NOT IN (?)", o.OmitTypes) s = s.Where("files.file_type NOT IN (?)", o.OmitTypes)
} }
// Primary files only? // Primary files only?
if o.Primary { if o.Primary {
s = s.Where("file_primary = 1") s = s.Where("files.file_primary = 1")
} }
// Files in originals only? // Files in originals only?
if o.Originals { if o.Originals {
s = s.Where("file_root = '/'") s = s.Where("files.file_root = '/'")
} }
// Exclude private? // Exclude private?

View File

@@ -0,0 +1,121 @@
package query
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/form"
)
func TestFileSelection(t *testing.T) {
none := form.Selection{Photos: []string{}}
one := form.Selection{Photos: []string{"pt9jtdre2lvl0yh8"}}
two := form.Selection{Photos: []string{"pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"}}
albums := form.Selection{Albums: []string{"at9lxuqxpogaaba9", "at6axuzitogaaiax", "at9lxuqxpogaaba8", "at9lxuqxpogaaba7"}}
months := form.Selection{Albums: []string{"at1lxuqipogaabj9"}}
folders := form.Selection{Albums: []string{"at1lxuqipogaaba1", "at1lxuqipogaabj8"}}
states := form.Selection{Albums: []string{"at1lxuqipogaab11", "at1lxuqipotaab12", "at1lxuqipotaab19"}}
many := form.Selection{
Files: []string{"ft8es39w45bnlqdw"},
Photos: []string{"pt9jtdre2lvl0y21", "pt9jtdre2lvl0y19", "pr2xu7myk7wrbk38", "pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"},
}
t.Run("EmptySelection", func(t *testing.T) {
sel := DownloadSelection(true, false, true)
if results, err := SelectedFiles(none, sel); err == nil {
t.Fatal("error expected")
} else {
assert.Empty(t, results)
}
})
t.Run("DownloadSelectionRawSidecarPrivate", func(t *testing.T) {
sel := DownloadSelection(true, true, false)
if results, err := SelectedFiles(one, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 2)
}
})
t.Run("DownloadSelectionRawOriginals", func(t *testing.T) {
sel := DownloadSelection(true, false, true)
if results, err := SelectedFiles(two, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 2)
}
})
t.Run("ShareSelectionOriginals", func(t *testing.T) {
sel := ShareSelection(false)
if results, err := SelectedFiles(many, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 4)
}
})
t.Run("ShareSelectionPrimary", func(t *testing.T) {
sel := ShareSelection(true)
if results, err := SelectedFiles(many, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 6)
}
})
t.Run("ShareAlbums", func(t *testing.T) {
sel := ShareSelection(true)
if results, err := SelectedFiles(albums, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 8)
}
})
t.Run("ShareMonths", func(t *testing.T) {
sel := ShareSelection(true)
if results, err := SelectedFiles(months, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 0)
}
})
t.Run("ShareFoldersOriginals", func(t *testing.T) {
sel := ShareSelection(true)
if results, err := SelectedFiles(folders, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 4)
}
})
t.Run("ShareFolders", func(t *testing.T) {
sel := ShareSelection(false)
if results, err := SelectedFiles(folders, sel); err != nil {
t.Fatal(err)
} else {
log.Debugf("ShareFolders Results: %#v", results)
assert.Len(t, results, 2)
}
})
t.Run("ShareStatesOriginals", func(t *testing.T) {
sel := ShareSelection(true)
if results, err := SelectedFiles(states, sel); err != nil {
t.Fatal(err)
} else {
assert.Len(t, results, 3)
}
})
t.Run("ShareStates", func(t *testing.T) {
sel := ShareSelection(false)
if results, err := SelectedFiles(states, sel); err != nil {
t.Fatal(err)
} else {
log.Debugf("ShareStates Result: %#v", results[0])
assert.Len(t, results, 1)
}
})
}

View File

@@ -14,6 +14,13 @@ func SelectedPhotos(f form.Selection) (results entity.Photos, err error) {
return results, errors.New("no items selected") return results, errors.New("no items selected")
} }
// Resolve photos in smart albums.
if photoIds, err := AlbumsPhotoUIDs(f.Albums, false, false); err != nil {
log.Warnf("query: %s", err.Error())
} else if len(photoIds) > 0 {
f.Photos = append(f.Photos, photoIds...)
}
var concat string var concat string
switch DbDialect() { switch DbDialect() {

View File

@@ -3,12 +3,21 @@ package query
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/stretchr/testify/assert"
) )
func TestPhotoSelection(t *testing.T) { func TestPhotoSelection(t *testing.T) {
albums := form.Selection{Albums: []string{"at9lxuqxpogaaba9", "at6axuzitogaaiax", "at9lxuqxpogaaba8", "at9lxuqxpogaaba7"}}
months := form.Selection{Albums: []string{"at1lxuqipogaabj9"}}
folders := form.Selection{Albums: []string{"at1lxuqipogaaba1", "at1lxuqipogaabj8"}}
states := form.Selection{Albums: []string{"at1lxuqipogaab11", "at1lxuqipotaab12", "at1lxuqipotaab19"}}
t.Run("no items selected", func(t *testing.T) { t.Run("no items selected", func(t *testing.T) {
f := form.Selection{ f := form.Selection{
Photos: []string{}, Photos: []string{},
@@ -33,58 +42,44 @@ func TestPhotoSelection(t *testing.T) {
assert.Equal(t, 2, len(r)) assert.Equal(t, 2, len(r))
assert.IsType(t, entity.Photos{}, r) assert.IsType(t, entity.Photos{}, r)
}) })
} t.Run("FindAlbums", func(t *testing.T) {
r, err := SelectedPhotos(albums)
func TestFileSelection(t *testing.T) { if err != nil {
none := form.Selection{Photos: []string{}}
one := form.Selection{Photos: []string{"pt9jtdre2lvl0yh8"}}
two := form.Selection{Photos: []string{"pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"}}
many := form.Selection{
Files: []string{"ft8es39w45bnlqdw"},
Photos: []string{"pt9jtdre2lvl0y21", "pt9jtdre2lvl0y19", "pr2xu7myk7wrbk38", "pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"},
}
t.Run("EmptySelection", func(t *testing.T) {
sel := DownloadSelection(true, false, true)
if results, err := SelectedFiles(none, sel); err == nil {
t.Fatal("error expected")
} else {
assert.Empty(t, results)
}
})
t.Run("DownloadSelectionRawSidecarPrivate", func(t *testing.T) {
sel := DownloadSelection(true, true, false)
if results, err := SelectedFiles(one, sel); err != nil {
t.Fatal(err) t.Fatal(err)
} else {
assert.Len(t, results, 2)
} }
assert.Equal(t, 6, len(r))
assert.IsType(t, entity.Photos{}, r)
}) })
t.Run("DownloadSelectionRawOriginals", func(t *testing.T) { t.Run("FindMonths", func(t *testing.T) {
sel := DownloadSelection(true, false, true) r, err := SelectedPhotos(months)
if results, err := SelectedFiles(two, sel); err != nil {
if err != nil {
t.Fatal(err) t.Fatal(err)
} else {
assert.Len(t, results, 2)
} }
assert.Equal(t, 0, len(r))
assert.IsType(t, entity.Photos{}, r)
}) })
t.Run("ShareSelectionOriginals", func(t *testing.T) { t.Run("FindFolders", func(t *testing.T) {
sel := ShareSelection(false) r, err := SelectedPhotos(folders)
if results, err := SelectedFiles(many, sel); err != nil {
if err != nil {
t.Fatal(err) t.Fatal(err)
} else {
assert.Len(t, results, 6)
} }
assert.Equal(t, 2, len(r))
assert.IsType(t, entity.Photos{}, r)
}) })
t.Run("ShareSelectionPrimary", func(t *testing.T) { t.Run("FindStates", func(t *testing.T) {
sel := ShareSelection(true) r, err := SelectedPhotos(states)
if results, err := SelectedFiles(many, sel); err != nil {
if err != nil {
t.Fatal(err) t.Fatal(err)
} else {
assert.Len(t, results, 4)
} }
assert.Equal(t, 1, len(r))
assert.IsType(t, entity.Photos{}, r)
}) })
} }

View File

@@ -31,6 +31,13 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
return searchPhotos(f, PhotosColsAll) return searchPhotos(f, PhotosColsAll)
} }
// PhotoIds finds photo and file ids based on the search form provided and returns them as PhotoResults.
func PhotoIds(f form.SearchPhotos) (files PhotoResults, count int, err error) {
f.Merged = false
f.Primary = true
return searchPhotos(f, "photos.id, photos.photo_uid, files.file_uid")
}
// photos searches for photos based on a Form and returns PhotoResults ([]Photo). // photos searches for photos based on a Form and returns PhotoResults ([]Photo).
func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults, count int, err error) { func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults, count int, err error) {
start := time.Now() start := time.Now()

View File

@@ -5,6 +5,8 @@ import (
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
@@ -78,6 +80,16 @@ func (worker *Share) Start() (err error) {
continue continue
} }
size := thumb.Size{}
if a.ShareSize != "" {
if s, ok := thumb.Sizes[thumb.Name(a.ShareSize)]; ok {
size = s
} else {
size = thumb.Sizes[thumb.Fit2048]
}
}
client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout)) client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout))
existingDirs := make(map[string]string) existingDirs := make(map[string]string)
@@ -97,14 +109,7 @@ func (worker *Share) Start() (err error) {
srcFileName := photoprism.FileName(file.File.FileRoot, file.File.FileName) srcFileName := photoprism.FileName(file.File.FileRoot, file.File.FileName)
if a.ShareSize != "" { if fs.ImageJPEG.Equal(file.File.FileType) && size.Width > 0 && size.Height > 0 {
size, ok := thumb.Sizes[thumb.Name(a.ShareSize)]
if !ok {
log.Errorf("share: invalid size %s", a.ShareSize)
continue
}
srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbCachePath(), size.Width, size.Height, file.File.FileOrientation, size.Options...) srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbCachePath(), size.Width, size.Height, file.File.FileOrientation, size.Options...)
if err != nil { if err != nil {