Import: Implement "add to album" in backend #246
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
a1238c94cc
commit
c39ec9695f
23 changed files with 288 additions and 72 deletions
|
@ -272,27 +272,19 @@ func AddPhotosToAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
var added []entity.PhotoAlbum
|
||||
added := a.AddPhotos(photos.UIDs())
|
||||
|
||||
for _, p := range photos {
|
||||
pa := entity.PhotoAlbum{AlbumUID: a.AlbumUID, PhotoUID: p.PhotoUID, Hidden: false}
|
||||
|
||||
if err := pa.Save(); err != nil {
|
||||
log.Errorf("album: %s", err.Error())
|
||||
if len(added) > 0 {
|
||||
if len(added) == 1 {
|
||||
event.Success(fmt.Sprintf("one entry added to %s", txt.Quote(a.Title())))
|
||||
} else {
|
||||
added = append(added, pa)
|
||||
event.Success(fmt.Sprintf("%d entries added to %s", len(added), txt.Quote(a.Title())))
|
||||
}
|
||||
|
||||
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
|
||||
}
|
||||
|
||||
if len(added) == 1 {
|
||||
event.Success(fmt.Sprintf("one photo added to %s", txt.Quote(a.AlbumTitle)))
|
||||
} else {
|
||||
event.Success(fmt.Sprintf("%d photos added to %s", len(added), txt.Quote(a.AlbumTitle)))
|
||||
}
|
||||
|
||||
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "added": added})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "photos added to album", "album": a, "photos": photos.UIDs(), "added": added})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -324,18 +316,19 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
for _, photoUID := range f.Photos {
|
||||
pa := entity.PhotoAlbum{AlbumUID: a.AlbumUID, PhotoUID: photoUID, Hidden: true}
|
||||
logError("album", pa.Save())
|
||||
removed := a.RemovePhotos(f.Photos)
|
||||
|
||||
if len(removed) > 0 {
|
||||
if len(removed) == 1 {
|
||||
event.Success(fmt.Sprintf("one entry removed from %s", txt.Quote(a.Title())))
|
||||
} else {
|
||||
event.Success(fmt.Sprintf("%d entries removed from %s", len(removed), txt.Quote(txt.Quote(a.Title()))))
|
||||
}
|
||||
|
||||
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
|
||||
}
|
||||
|
||||
// affected := entity.Db().Model(entity.PhotoAlbum{}).Where("album_uid = ? AND photo_uid IN (?)", a.AlbumUID, f.Photos).UpdateColumn("Hidden", true).RowsAffected
|
||||
|
||||
event.Success(fmt.Sprintf("entries removed from %s", a.AlbumTitle))
|
||||
|
||||
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "entries removed from album", "album": a, "photos": f.Photos})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "entries removed from album", "album": a, "photos": f.Photos, "removed": removed})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,11 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
|
|||
opt = photoprism.ImportOptionsCopy(path)
|
||||
}
|
||||
|
||||
if len(f.Albums) > 0 {
|
||||
log.Debugf("import: files will be added to album %s", strings.Join(f.Albums, " and "))
|
||||
opt.Albums = f.Albums
|
||||
}
|
||||
|
||||
imp.Start(opt)
|
||||
|
||||
if subPath != "" && path != conf.ImportPath() && fs.IsEmpty(path) {
|
||||
|
|
|
@ -19,6 +19,8 @@ const (
|
|||
AccountSyncStatusSynced = "synced"
|
||||
)
|
||||
|
||||
type Accounts []Account
|
||||
|
||||
// Account represents a remote service account for uploading, downloading or syncing media files.
|
||||
type Account struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -13,6 +14,8 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
type Albums []Album
|
||||
|
||||
// Album represents a photo album
|
||||
type Album struct {
|
||||
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
||||
|
@ -40,13 +43,49 @@ type Album struct {
|
|||
DeletedAt *time.Time `sql:"index" json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsUID(m.AlbumUID, 'a') {
|
||||
// AddPhotoToAlbums adds a photo UID to multiple albums and automatically creates them if needed.
|
||||
func AddPhotoToAlbums(photo string, albums []string) (err error) {
|
||||
if photo == "" || len(albums) == 0 {
|
||||
// Do nothing.
|
||||
return nil
|
||||
}
|
||||
|
||||
return scope.SetColumn("AlbumUID", rnd.PPID('a'))
|
||||
if !rnd.IsPPID(photo, 'p') {
|
||||
return fmt.Errorf("album: invalid photo uid %s", photo)
|
||||
}
|
||||
|
||||
for _, album := range albums {
|
||||
var aUID string
|
||||
|
||||
if album == "" {
|
||||
log.Debugf("album: empty album identifier while adding photo %s", photo)
|
||||
continue
|
||||
}
|
||||
|
||||
if rnd.IsPPID(album, 'a') {
|
||||
aUID = album
|
||||
} else {
|
||||
a := NewAlbum(album, TypeAlbum)
|
||||
|
||||
if err = a.Find(); err == nil {
|
||||
aUID = a.AlbumUID
|
||||
} else if err = a.Create(); err == nil {
|
||||
aUID = a.AlbumUID
|
||||
} else {
|
||||
log.Errorf("album: %s (add photo %s to albums)", err.Error(), photo)
|
||||
}
|
||||
}
|
||||
|
||||
if aUID != "" {
|
||||
entry := PhotoAlbum{AlbumUID: aUID, PhotoUID: photo, Hidden: false}
|
||||
|
||||
if err = entry.Save(); err != nil {
|
||||
log.Errorf("album: %s (add photo %s to albums)", err.Error(), photo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewAlbum creates a new album; default name is current month and year
|
||||
|
@ -58,7 +97,6 @@ func NewAlbum(albumTitle, albumType string) *Album {
|
|||
}
|
||||
|
||||
result := &Album{
|
||||
AlbumUID: rnd.PPID('a'),
|
||||
AlbumOrder: SortOrderOldest,
|
||||
AlbumType: albumType,
|
||||
CreatedAt: now,
|
||||
|
@ -79,7 +117,6 @@ func NewFolderAlbum(albumTitle, albumSlug, albumFilter string) *Album {
|
|||
now := time.Now().UTC()
|
||||
|
||||
result := &Album{
|
||||
AlbumUID: rnd.PPID('a'),
|
||||
AlbumOrder: SortOrderOldest,
|
||||
AlbumType: TypeFolder,
|
||||
AlbumTitle: albumTitle,
|
||||
|
@ -101,7 +138,6 @@ func NewMomentsAlbum(albumTitle, albumSlug, albumFilter string) *Album {
|
|||
now := time.Now().UTC()
|
||||
|
||||
result := &Album{
|
||||
AlbumUID: rnd.PPID('a'),
|
||||
AlbumOrder: SortOrderOldest,
|
||||
AlbumType: TypeMoment,
|
||||
AlbumTitle: albumTitle,
|
||||
|
@ -128,7 +164,6 @@ func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
|
|||
now := time.Now().UTC()
|
||||
|
||||
result := &Album{
|
||||
AlbumUID: rnd.PPID('a'),
|
||||
AlbumOrder: SortOrderOldest,
|
||||
AlbumType: TypeMonth,
|
||||
AlbumTitle: albumTitle,
|
||||
|
@ -143,6 +178,59 @@ func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
|
|||
return result
|
||||
}
|
||||
|
||||
// FindAlbumBySlug finds a matching album or returns nil.
|
||||
func FindAlbumBySlug(slug, albumType string) *Album {
|
||||
result := Album{}
|
||||
|
||||
if err := UnscopedDb().Where("album_slug = ? AND album_type = ?", slug, albumType).First(&result).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Find updates the entity with values from the database.
|
||||
func (m *Album) Find() error {
|
||||
if rnd.IsPPID(m.AlbumUID, 'a') {
|
||||
log.Debugf("IS PPID: %s", m.AlbumUID)
|
||||
if err := UnscopedDb().First(m, "album_uid = ?", m.AlbumUID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := UnscopedDb().First(m, "album_slug = ? AND album_type = ?", m.AlbumSlug, m.AlbumType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsUID(m.AlbumUID, 'a') {
|
||||
return nil
|
||||
}
|
||||
|
||||
return scope.SetColumn("AlbumUID", rnd.PPID('a'))
|
||||
}
|
||||
|
||||
// String returns the id or name as string.
|
||||
func (m *Album) String() string {
|
||||
if m.AlbumSlug != "" {
|
||||
return m.AlbumSlug
|
||||
}
|
||||
|
||||
if m.AlbumTitle != "" {
|
||||
return txt.Quote(m.AlbumTitle)
|
||||
}
|
||||
|
||||
if m.AlbumUID != "" {
|
||||
return m.AlbumUID
|
||||
}
|
||||
|
||||
return "[unknown album]"
|
||||
}
|
||||
|
||||
// Checks if the album is of type moment.
|
||||
func (m *Album) IsMoment() bool {
|
||||
return m.AlbumType == TypeMoment
|
||||
|
@ -213,13 +301,37 @@ func (m *Album) Create() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// FindAlbum finds a matching album or returns nil.
|
||||
func FindAlbum(slug, albumType string) *Album {
|
||||
result := Album{}
|
||||
// Returns the album title.
|
||||
func (m *Album) Title() string {
|
||||
return m.AlbumTitle
|
||||
}
|
||||
|
||||
if err := UnscopedDb().Where("album_slug = ? AND album_type = ?", slug, albumType).First(&result).Error; err != nil {
|
||||
return nil
|
||||
// AddPhotos adds photos to an existing album.
|
||||
func (m *Album) AddPhotos(UIDs []string) (added []PhotoAlbum) {
|
||||
for _, uid := range UIDs {
|
||||
entry := PhotoAlbum{AlbumUID: m.AlbumUID, PhotoUID: uid, Hidden: false}
|
||||
|
||||
if err := entry.Save(); err != nil {
|
||||
log.Errorf("album: %s (add to album %s)", err.Error(), m)
|
||||
} else {
|
||||
added = append(added, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
return added
|
||||
}
|
||||
|
||||
// RemovePhotos removes photos from an album.
|
||||
func (m *Album) RemovePhotos(UIDs []string) (removed []PhotoAlbum) {
|
||||
for _, uid := range UIDs {
|
||||
entry := PhotoAlbum{AlbumUID: m.AlbumUID, PhotoUID: uid, Hidden: true}
|
||||
|
||||
if err := entry.Save(); err != nil {
|
||||
log.Errorf("album: %s (remove from album %s)", err.Error(), m)
|
||||
} else {
|
||||
removed = append(removed, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
|
|
@ -92,6 +92,24 @@ var AlbumFixtures = AlbumMap{
|
|||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
},
|
||||
"import": {
|
||||
ID: 1000004,
|
||||
CoverUID: "",
|
||||
AlbumUID: "at6axuzitogaaiax",
|
||||
AlbumSlug: "import",
|
||||
AlbumType: TypeAlbum,
|
||||
AlbumTitle: "Import Album",
|
||||
AlbumDescription: "",
|
||||
AlbumNotes: "",
|
||||
AlbumOrder: "name",
|
||||
AlbumTemplate: "",
|
||||
AlbumFilter: "",
|
||||
AlbumFavorite: false,
|
||||
Links: []Link{},
|
||||
CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
DeletedAt: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// CreateAlbumFixtures inserts known entities into the database for testing.
|
||||
|
|
|
@ -89,3 +89,31 @@ func TestAlbum_Save(t *testing.T) {
|
|||
})
|
||||
|
||||
}
|
||||
|
||||
func TestAddPhotoToAlbums(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
err := AddPhotoToAlbums("pt9jtxrexxvl0yh0", []string{"at6axuzitogaaiax"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a := Album{AlbumUID: "at6axuzitogaaiax"}
|
||||
|
||||
if err := a.Find(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var entries []PhotoAlbum
|
||||
|
||||
if err := Db().Where("album_uid = ? AND photo_uid = ?", "at6axuzitogaaiax", "pt9jtxrexxvl0yh0").Find(&entries).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(entries) < 1 {
|
||||
t.Error("at least one album entry expected")
|
||||
}
|
||||
|
||||
// t.Logf("photo album entries: %+v", entries)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -41,4 +41,8 @@ const (
|
|||
RootDefault = ""
|
||||
RootImport = "import"
|
||||
RootPath = "/"
|
||||
|
||||
Updated = "updated"
|
||||
Created = "created"
|
||||
Deleted = "deleted"
|
||||
)
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
type Files []File
|
||||
|
||||
// File represents an image or sidecar file that belongs to a photo.
|
||||
type File struct {
|
||||
ID uint `gorm:"primary_key" json:"-" yaml:"-"`
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
type Folders []Folder
|
||||
|
||||
// Folder represents a file system directory.
|
||||
type Folder struct {
|
||||
Path string `gorm:"type:varbinary(255);unique_index:idx_folders_path_root;" json:"Path" yaml:"Path"`
|
||||
|
|
|
@ -17,6 +17,20 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
// Default photo result slice for simple use cases.
|
||||
type Photos []Photo
|
||||
|
||||
// UIDs returns a slice of unique photo IDs.
|
||||
func (m Photos) UIDs() []string {
|
||||
result := make([]string, len(m))
|
||||
|
||||
for i, el := range m {
|
||||
result[i] = el.PhotoUID
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Photo represents a photo, all its properties, and link to all its images and sidecar files.
|
||||
type Photo struct {
|
||||
ID uint `gorm:"primary_key" yaml:"-"`
|
||||
|
@ -132,7 +146,7 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// String returns an entity identifier as string for use in logs.
|
||||
// String returns the id or name as string.
|
||||
func (m *Photo) String() string {
|
||||
if m.PhotoUID == "" {
|
||||
if m.PhotoName != "" {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package form
|
||||
|
||||
type ImportOptions struct {
|
||||
Path string `json:"path"`
|
||||
Move bool `json:"move"`
|
||||
Albums []string `json:"albums"`
|
||||
Path string `json:"path"`
|
||||
Move bool `json:"move"`
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package photoprism
|
||||
|
||||
type ImportOptions struct {
|
||||
Albums []string
|
||||
Path string
|
||||
Move bool
|
||||
RemoveDotFiles bool
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
@ -129,6 +130,13 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
}
|
||||
|
||||
res := ind.MediaFile(related.Main, indexOpt, originalName)
|
||||
|
||||
if res.Success() {
|
||||
if err := entity.AddPhotoToAlbums(res.PhotoUID, opt.Albums); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("import: %s main %s file %s", res, related.Main.FileType(), txt.Quote(related.Main.RelativeName(ind.originalsPath())))
|
||||
done[related.Main.FileName()] = true
|
||||
} else {
|
||||
|
@ -149,6 +157,8 @@ func ImportWorker(jobs <-chan ImportJob) {
|
|||
|
||||
log.Infof("import: %s related %s file %s", res, f.FileType(), txt.Quote(f.RelativeName(ind.originalsPath())))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ func (m *Moments) Start() (err error) {
|
|||
Path: mom.Path,
|
||||
}
|
||||
|
||||
if a := entity.FindAlbum(mom.Slug(), entity.TypeFolder); a != nil {
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeFolder); a != nil {
|
||||
if a.DeletedAt != nil {
|
||||
// Nothing to do.
|
||||
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
|
||||
|
@ -101,7 +101,7 @@ func (m *Moments) Start() (err error) {
|
|||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
for _, mom := range results {
|
||||
if a := entity.FindAlbum(mom.Slug(), entity.TypeMonth); a != nil {
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMonth); a != nil {
|
||||
if a.DeletedAt != nil {
|
||||
// Nothing to do.
|
||||
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
|
||||
|
@ -128,7 +128,7 @@ func (m *Moments) Start() (err error) {
|
|||
Year: mom.Year,
|
||||
}
|
||||
|
||||
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMoment); a != nil {
|
||||
if a.DeletedAt != nil {
|
||||
// Nothing to do.
|
||||
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
|
||||
|
@ -158,7 +158,7 @@ func (m *Moments) Start() (err error) {
|
|||
State: mom.State,
|
||||
}
|
||||
|
||||
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMoment); a != nil {
|
||||
if a.DeletedAt != nil {
|
||||
// Nothing to do.
|
||||
log.Debugf("moments: %s was deleted (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
|
||||
|
@ -186,7 +186,7 @@ func (m *Moments) Start() (err error) {
|
|||
Label: mom.Label,
|
||||
}
|
||||
|
||||
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.TypeMoment); a != nil {
|
||||
log.Debugf("moments: %s already exists (%s)", txt.Quote(mom.Title()), f.Serialize())
|
||||
|
||||
if f.Serialize() == a.AlbumFilter || a.DeletedAt != nil {
|
||||
|
|
|
@ -5,10 +5,8 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
type Accounts []entity.Account
|
||||
|
||||
// AccountSearch returns a list of accounts.
|
||||
func AccountSearch(f form.AccountSearch) (result Accounts, err error) {
|
||||
func AccountSearch(f form.AccountSearch) (result entity.Accounts, err error) {
|
||||
s := Db().Where(&entity.Account{})
|
||||
|
||||
if f.Share {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
)
|
||||
|
||||
// AccountUploads a list of files for uploading to a remote account.
|
||||
func AccountUploads(a entity.Account, limit int) (results []entity.File, err error) {
|
||||
func AccountUploads(a entity.Account, limit int) (results entity.Files, err error) {
|
||||
s := Db().Where("files.file_missing = 0").
|
||||
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
|
||||
|
||||
|
|
|
@ -6,10 +6,8 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
type Files []entity.File
|
||||
|
||||
// FilesByPath returns a slice of files in a given originals folder.
|
||||
func FilesByPath(rootName, pathName string) (files Files, err error) {
|
||||
func FilesByPath(rootName, pathName string) (files entity.Files, err error) {
|
||||
if strings.HasPrefix(pathName, "/") {
|
||||
pathName = pathName[1:]
|
||||
}
|
||||
|
@ -26,7 +24,7 @@ func FilesByPath(rootName, pathName string) (files Files, err error) {
|
|||
}
|
||||
|
||||
// ExistingFiles returns not-missing and not-deleted file entities in the range of limit and offset sorted by id.
|
||||
func ExistingFiles(limit int, offset int, pathName string) (files Files, err error) {
|
||||
func ExistingFiles(limit int, offset int, pathName string) (files entity.Files, err error) {
|
||||
if strings.HasPrefix(pathName, "/") {
|
||||
pathName = pathName[1:]
|
||||
}
|
||||
|
@ -43,7 +41,7 @@ func ExistingFiles(limit int, offset int, pathName string) (files Files, err err
|
|||
}
|
||||
|
||||
// FilesByUID
|
||||
func FilesByUID(u []string, limit int, offset int) (files Files, err error) {
|
||||
func FilesByUID(u []string, limit int, offset int) (files entity.Files, err error) {
|
||||
if err := Db().Where("(photo_uid IN (?) AND file_primary = 1) OR file_uid IN (?)", u, u).Preload("Photo").Limit(limit).Offset(offset).Find(&files).Error; err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
|
|
@ -7,17 +7,15 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
type Folders []entity.Folder
|
||||
|
||||
// FoldersByPath returns a slice of folders in a given directory incl sub directories in recursive mode.
|
||||
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders Folders, err error) {
|
||||
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders entity.Folders, err error) {
|
||||
dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive)
|
||||
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
|
||||
folders = make(Folders, len(dirs))
|
||||
folders = make(entity.Folders, len(dirs))
|
||||
|
||||
for i, dir := range dirs {
|
||||
folder := entity.FindFolder(rootName, filepath.Join(path, dir))
|
||||
|
@ -39,7 +37,7 @@ func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders Fol
|
|||
}
|
||||
|
||||
// AlbumFolders returns folders that should be added as album.
|
||||
func AlbumFolders(threshold int) (folders Folders, err error) {
|
||||
func AlbumFolders(threshold int) (folders entity.Folders, err error) {
|
||||
db := UnscopedDb().Table("folders").
|
||||
Select("folders.*, COUNT(photos.id) AS photo_count").
|
||||
Joins("JOIN photos ON photos.photo_path = folders.path AND photos.deleted_at IS NULL AND photos.photo_quality >= 3").
|
||||
|
|
|
@ -73,7 +73,7 @@ func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) {
|
|||
}
|
||||
|
||||
// PhotosMissing returns photo entities without existing files.
|
||||
func PhotosMissing(limit int, offset int) (entities Photos, err error) {
|
||||
func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) {
|
||||
err = Db().
|
||||
Select("photos.*").
|
||||
Joins("JOIN files a ON photos.id = a.photo_id ").
|
||||
|
@ -94,7 +94,7 @@ func ResetPhotoQuality() error {
|
|||
}
|
||||
|
||||
// PhotosMaintenance returns photos selected for maintenance.
|
||||
func PhotosMaintenance(limit int, offset int) (entities Photos, err error) {
|
||||
func PhotosMaintenance(limit int, offset int) (entities entity.Photos, err error) {
|
||||
err = Db().
|
||||
Preload("Labels", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("photos_labels.uncertainty ASC, photos_labels.label_id DESC")
|
||||
|
|
|
@ -11,8 +11,6 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
// Default photo result slice for simple use cases.
|
||||
type Photos []entity.Photo
|
||||
|
||||
// PhotoResult contains found photos and their main file plus other meta data.
|
||||
type PhotoResult struct {
|
||||
|
|
|
@ -4,11 +4,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
// PhotoSelection queries all selected photos.
|
||||
func PhotoSelection(f form.Selection) (results Photos, err error) {
|
||||
func PhotoSelection(f form.Selection) (results entity.Photos, err error) {
|
||||
if f.Empty() {
|
||||
return results, errors.New("no items selected")
|
||||
}
|
||||
|
@ -47,7 +48,7 @@ func PhotoSelection(f form.Selection) (results Photos, err error) {
|
|||
}
|
||||
|
||||
// FileSelection queries all selected files e.g. for downloading.
|
||||
func FileSelection(f form.Selection) (results Files, err error) {
|
||||
func FileSelection(f form.Selection) (results entity.Files, err error) {
|
||||
if f.Empty() {
|
||||
return results, errors.New("no items selected")
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package query
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -30,7 +31,7 @@ func TestPhotoSelection(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, 2, len(r))
|
||||
assert.IsType(t, Photos{}, r)
|
||||
assert.IsType(t, entity.Photos{}, r)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -57,6 +58,6 @@ func TestFileSelection(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, 3, len(r))
|
||||
assert.IsType(t, Files{}, r)
|
||||
assert.IsType(t, entity.Files{}, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,13 +21,43 @@ func IsPPID(s string, prefix byte) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return s[0] == prefix
|
||||
return s[0] == prefix && IsLowerAlnum(s)
|
||||
}
|
||||
|
||||
// IsHex returns true if the string only contains hex numbers, dashes and letters without whitespace.
|
||||
func IsHex(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 90) && r != 45{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsLowerAlnum returns true if the string only contains alphanumeric ascii chars without whitespace.
|
||||
func IsLowerAlnum(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if (r < 48 || r > 57) && (r < 97 || r > 122) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsUID returns true if string is a seemingly unique id.
|
||||
func IsUID(s string, prefix byte) bool {
|
||||
// Regular UUID.
|
||||
if len(s) == 36 {
|
||||
if len(s) == 36 && IsHex(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue