API: Add action and user context to indexing events #98

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2023-03-11 14:09:00 +01:00
parent a5f4cce181
commit dcffa2848a
15 changed files with 127 additions and 38 deletions

View File

@@ -105,7 +105,7 @@ func StartImport(router *gin.RouterGroup) {
// Set user UID if known. // Set user UID if known.
if s.UserUID != "" { if s.UserUID != "" {
opt.UserUID = s.UserUID opt.UID = s.UserUID
} }
// Start import. // Start import.
@@ -138,8 +138,16 @@ func StartImport(router *gin.RouterGroup) {
msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed) msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed)
event.Success(msg) event.Success(msg)
event.Publish("import.completed", event.Data{"path": importPath, "seconds": elapsed})
event.Publish("index.completed", event.Data{"path": importPath, "seconds": elapsed}) eventData := event.Data{
"uid": opt.UID,
"action": opt.Action,
"path": importPath,
"seconds": elapsed,
}
event.Publish("import.completed", eventData)
event.Publish("index.completed", eventData)
for _, uid := range f.Albums { for _, uid := range f.Albums {
PublishAlbumEvent(EntityUpdated, uid, c) PublishAlbumEvent(EntityUpdated, uid, c)

View File

@@ -53,6 +53,7 @@ func StartIndexing(router *gin.RouterGroup) {
skipArchived := settings.Index.SkipArchived skipArchived := settings.Index.SkipArchived
indOpt := photoprism.NewIndexOptions(filepath.Clean(f.Path), f.Rescan, convert, true, false, skipArchived) indOpt := photoprism.NewIndexOptions(filepath.Clean(f.Path), f.Rescan, convert, true, false, skipArchived)
indOpt.SetUser(s.User())
if len(indOpt.Path) > 1 { if len(indOpt.Path) > 1 {
event.InfoMsg(i18n.MsgIndexingFiles, clean.Log(indOpt.Path)) event.InfoMsg(i18n.MsgIndexingFiles, clean.Log(indOpt.Path))
@@ -76,13 +77,17 @@ func StartIndexing(router *gin.RouterGroup) {
// Update index? // Update index?
if updateIndex { if updateIndex {
event.Publish("index.updating", event.Data{ event.Publish("index.updating", event.Data{
"step": "folders", "uid": indOpt.UID,
"action": indOpt.Action,
"step": "folders",
}) })
RemoveFromFolderCache(entity.RootOriginals) RemoveFromFolderCache(entity.RootOriginals)
event.Publish("index.updating", event.Data{ event.Publish("index.updating", event.Data{
"step": "purge", "uid": indOpt.UID,
"action": indOpt.Action,
"step": "purge",
}) })
// Configure purge options. // Configure purge options.
@@ -107,7 +112,9 @@ func StartIndexing(router *gin.RouterGroup) {
// Update moments? // Update moments?
if forceUpdate { if forceUpdate {
event.Publish("index.updating", event.Data{ event.Publish("index.updating", event.Data{
"step": "moments", "uid": indOpt.UID,
"action": indOpt.Action,
"step": "moments",
}) })
moments := get.Moments() moments := get.Moments()
@@ -122,7 +129,12 @@ func StartIndexing(router *gin.RouterGroup) {
msg := i18n.Msg(i18n.MsgIndexingCompletedIn, elapsed) msg := i18n.Msg(i18n.MsgIndexingCompletedIn, elapsed)
event.Success(msg) event.Success(msg)
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed}) event.Publish("index.completed", event.Data{
"uid": indOpt.UID,
"action": indOpt.Action,
"path": path,
"seconds": elapsed,
})
UpdateClientConfig() UpdateClientConfig()

View File

@@ -9,7 +9,6 @@ import (
"github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/i18n"
@@ -145,8 +144,6 @@ func AddService(router *gin.RouterGroup) {
return return
} }
event.SuccessMsg(i18n.MsgAccountCreated)
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
}) })
} }
@@ -201,8 +198,6 @@ func UpdateService(router *gin.RouterGroup) {
return return
} }
event.SuccessMsg(i18n.MsgAccountSaved)
m, err = query.AccountByID(id) m, err = query.AccountByID(id)
if err != nil { if err != nil {
@@ -250,8 +245,6 @@ func DeleteService(router *gin.RouterGroup) {
return return
} }
event.SuccessMsg(i18n.MsgAccountDeleted)
c.JSON(http.StatusOK, m) c.JSON(http.StatusOK, m)
}) })
} }

View File

@@ -41,7 +41,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
// Users may only change their own avatar. // Users may only change their own avatar.
if !isPrivileged && s.User().UserUID != uid { if !isPrivileged && s.User().UserUID != uid {
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "user uid does not match"}, s.RefID) event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "user does not match"}, s.RefID)
AbortForbidden(c) AbortForbidden(c)
return return
} }

View File

@@ -7,7 +7,6 @@ import (
"github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
@@ -75,8 +74,6 @@ func UpdateUser(router *gin.RouterGroup) {
// Clear the session cache, as it contains user information. // Clear the session cache, as it contains user information.
s.ClearCache() s.ClearCache()
event.SuccessMsg(i18n.MsgChangesSaved)
m = entity.FindUserByUID(uid) m = entity.FindUserByUID(uid)
if m == nil { if m == nil {

View File

@@ -29,11 +29,13 @@ func UploadUserFiles(router *gin.RouterGroup) {
router.POST("/users/:uid/upload/:token", func(c *gin.Context) { router.POST("/users/:uid/upload/:token", func(c *gin.Context) {
conf := get.Config() conf := get.Config()
// Abort in public mode or when the upload feature is disabled.
if conf.ReadOnly() || !conf.Settings().Features.Upload { if conf.ReadOnly() || !conf.Settings().Features.Upload {
Abort(c, http.StatusForbidden, i18n.ErrReadOnly) Abort(c, http.StatusForbidden, i18n.ErrReadOnly)
return return
} }
// Check permission.
s := AuthAny(c, acl.ResourceFiles, acl.Permissions{acl.ActionManage, acl.ActionUpload}) s := AuthAny(c, acl.ResourceFiles, acl.Permissions{acl.ActionManage, acl.ActionUpload})
if s.Abort(c) { if s.Abort(c) {
@@ -44,7 +46,7 @@ func UploadUserFiles(router *gin.RouterGroup) {
// Users may only upload their own files. // Users may only upload their own files.
if s.User().UserUID != uid { if s.User().UserUID != uid {
event.AuditErr([]string{ClientIP(c), "session %s", "upload files", "user uid does not match"}, s.RefID) event.AuditErr([]string{ClientIP(c), "session %s", "upload files", "user does not match"}, s.RefID)
AbortForbidden(c) AbortForbidden(c)
return return
} }
@@ -60,13 +62,15 @@ func UploadUserFiles(router *gin.RouterGroup) {
return return
} }
event.Publish("upload.start", event.Data{"time": start}) // Publish upload start event.
event.Publish("upload.start", event.Data{"uid": s.UserUID, "time": start})
files := f.File["files"] files := f.File["files"]
uploaded := len(files) uploaded := len(files)
var uploads []string var uploads []string
// Compose upload path.
uploadDir, err := conf.UserUploadPath(s.UserUID, s.RefID+token) uploadDir, err := conf.UserUploadPath(s.UserUID, s.RefID+token)
if err != nil { if err != nil {
@@ -75,20 +79,24 @@ func UploadUserFiles(router *gin.RouterGroup) {
return return
} }
// Save uploaded files.
for _, file := range files { for _, file := range files {
filename := path.Join(uploadDir, filepath.Base(file.Filename)) fileName := filepath.Base(file.Filename)
filePath := path.Join(uploadDir, fileName)
log.Debugf("upload: saving file %s", clean.Log(file.Filename)) if err = c.SaveUploadedFile(file, filePath); err != nil {
log.Errorf("upload: failed saving file %s", clean.Log(fileName))
if err := c.SaveUploadedFile(file, filename); err != nil {
log.Errorf("upload: failed saving file %s", clean.Log(filepath.Base(file.Filename)))
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed) Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
return return
} else {
log.Debugf("upload: saved file %s", clean.Log(fileName))
event.Publish("upload.saved", event.Data{"uid": s.UserUID, "file": fileName})
} }
uploads = append(uploads, filename) uploads = append(uploads, filePath)
} }
// Check if uploaded file is safe.
if !conf.UploadNSFW() { if !conf.UploadNSFW() {
nd := get.NsfwDetector() nd := get.NsfwDetector()
@@ -195,7 +203,7 @@ func ProcessUserUpload(router *gin.RouterGroup) {
// Set user UID if known. // Set user UID if known.
if s.UserUID != "" { if s.UserUID != "" {
opt.UserUID = s.UserUID opt.UID = s.UserUID
} }
// Start import. // Start import.
@@ -228,8 +236,9 @@ func ProcessUserUpload(router *gin.RouterGroup) {
msg := i18n.Msg(i18n.MsgUploadProcessed) msg := i18n.Msg(i18n.MsgUploadProcessed)
event.Success(msg) event.Success(msg)
event.Publish("import.completed", event.Data{"path": uploadPath, "seconds": elapsed}) event.Publish("import.completed", event.Data{"uid": opt.UID, "path": uploadPath, "seconds": elapsed})
event.Publish("index.completed", event.Data{"path": uploadPath, "seconds": elapsed}) event.Publish("index.completed", event.Data{"uid": opt.UID, "path": uploadPath, "seconds": elapsed})
event.Publish("upload.completed", event.Data{"uid": opt.UID, "path": uploadPath, "seconds": elapsed})
for _, uid := range f.Albums { for _, uid := range f.Albums {
PublishAlbumEvent(EntityUpdated, uid, c) PublishAlbumEvent(EntityUpdated, uid, c)

View File

@@ -69,6 +69,7 @@ func Import() error {
event.InfoMsg(i18n.MsgCopyingFilesFrom, clean.Log(filepath.Base(path))) event.InfoMsg(i18n.MsgCopyingFilesFrom, clean.Log(filepath.Base(path)))
var opt photoprism.ImportOptions var opt photoprism.ImportOptions
opt.Action = photoprism.ActionAutoImport
if conf.Settings().Import.Move { if conf.Settings().Import.Move {
opt = photoprism.ImportOptionsMove(path, conf.ImportDest()) opt = photoprism.ImportOptionsMove(path, conf.ImportDest())
@@ -93,8 +94,16 @@ func Import() error {
msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed) msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed)
event.Success(msg) event.Success(msg)
event.Publish("import.completed", event.Data{"path": path, "seconds": elapsed})
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed}) eventData := event.Data{
"uid": opt.UID,
"action": opt.Action,
"path": path,
"seconds": elapsed,
}
event.Publish("import.completed", eventData)
event.Publish("index.completed", eventData)
api.UpdateClientConfig() api.UpdateClientConfig()

View File

@@ -62,6 +62,7 @@ func Index() error {
convert := settings.Index.Convert && conf.SidecarWritable() convert := settings.Index.Convert && conf.SidecarWritable()
indOpt := photoprism.NewIndexOptions(entity.RootPath, false, convert, true, false, true) indOpt := photoprism.NewIndexOptions(entity.RootPath, false, convert, true, false, true)
indOpt.Action = photoprism.ActionAutoIndex
lastRun, lastFound := ind.LastRun() lastRun, lastFound := ind.LastRun()
found, indexed := ind.Start(indOpt) found, indexed := ind.Start(indOpt)
@@ -87,7 +88,9 @@ func Index() error {
} }
event.Publish("index.updating", event.Data{ event.Publish("index.updating", event.Data{
"step": "moments", "uid": indOpt.UID,
"action": indOpt.Action,
"step": "moments",
}) })
moments := get.Moments() moments := get.Moments()
@@ -101,7 +104,15 @@ func Index() error {
msg := i18n.Msg(i18n.MsgIndexingCompletedIn, elapsed) msg := i18n.Msg(i18n.MsgIndexingCompletedIn, elapsed)
event.Success(msg) event.Success(msg)
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
eventData := event.Data{
"uid": indOpt.UID,
"action": indOpt.Action,
"path": path,
"seconds": elapsed,
}
event.Publish("index.completed", eventData)
api.UpdateClientConfig() api.UpdateClientConfig()

View File

@@ -0,0 +1,10 @@
package photoprism
const (
ActionIndex = "index"
ActionAutoIndex = "autoindex"
ActionImport = "import"
ActionAutoImport = "autoimport"
ActionUpload = "upload"
ActionUnknown = ""
)

View File

@@ -107,6 +107,8 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
settings := imp.conf.Settings() settings := imp.conf.Settings()
convert := settings.Index.Convert && imp.conf.SidecarWritable() convert := settings.Index.Convert && imp.conf.SidecarWritable()
indexOpt := NewIndexOptions("/", true, convert, true, false, false) indexOpt := NewIndexOptions("/", true, convert, true, false, false)
indexOpt.UID = opt.UID
indexOpt.Action = opt.Action
skipRaw := imp.conf.DisableRaw() skipRaw := imp.conf.DisableRaw()
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false) ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)

View File

@@ -1,21 +1,35 @@
package photoprism package photoprism
import "github.com/photoprism/photoprism/internal/entity"
// ImportOptions represents file import options. // ImportOptions represents file import options.
type ImportOptions struct { type ImportOptions struct {
UID string
Action string
Albums []string Albums []string
Path string Path string
Move bool Move bool
NonBlocking bool NonBlocking bool
UserUID string
DestFolder string DestFolder string
RemoveDotFiles bool RemoveDotFiles bool
RemoveExistingFiles bool RemoveExistingFiles bool
RemoveEmptyDirectories bool RemoveEmptyDirectories bool
} }
// SetUser sets the user who performs the import operation.
func (o *ImportOptions) SetUser(user *entity.User) *ImportOptions {
if o != nil && user != nil {
o.UID = user.UID()
}
return o
}
// ImportOptionsCopy returns import options for copying files to originals (read-only). // ImportOptionsCopy returns import options for copying files to originals (read-only).
func ImportOptionsCopy(importPath, destFolder string) ImportOptions { func ImportOptionsCopy(importPath, destFolder string) ImportOptions {
result := ImportOptions{ result := ImportOptions{
UID: entity.Admin.UID(),
Action: ActionImport,
Path: importPath, Path: importPath,
Move: false, Move: false,
NonBlocking: false, NonBlocking: false,
@@ -31,6 +45,8 @@ func ImportOptionsCopy(importPath, destFolder string) ImportOptions {
// ImportOptionsMove returns import options for moving files to originals (modifies import directory). // ImportOptionsMove returns import options for moving files to originals (modifies import directory).
func ImportOptionsMove(importPath, destFolder string) ImportOptions { func ImportOptionsMove(importPath, destFolder string) ImportOptions {
result := ImportOptions{ result := ImportOptions{
UID: entity.Admin.UID(),
Action: ActionImport,
Path: importPath, Path: importPath,
Move: true, Move: true,
NonBlocking: false, NonBlocking: false,
@@ -46,6 +62,8 @@ func ImportOptionsMove(importPath, destFolder string) ImportOptions {
// ImportOptionsUpload returns options for importing user uploads. // ImportOptionsUpload returns options for importing user uploads.
func ImportOptionsUpload(uploadPath, destFolder string) ImportOptions { func ImportOptionsUpload(uploadPath, destFolder string) ImportOptions {
result := ImportOptions{ result := ImportOptions{
UID: entity.Admin.UID(),
Action: ActionUpload,
Path: uploadPath, Path: uploadPath,
Move: true, Move: true,
NonBlocking: true, NonBlocking: true,

View File

@@ -110,7 +110,7 @@ func ImportWorker(jobs <-chan ImportJob) {
// Do nothing. // Do nothing.
} else if file, err := entity.FirstFileByHash(fileHash); err != nil { } else if file, err := entity.FirstFileByHash(fileHash); err != nil {
// Do nothing. // Do nothing.
} else if err := entity.AddPhotoToUserAlbums(file.PhotoUID, opt.Albums, opt.UserUID); err != nil { } else if err := entity.AddPhotoToUserAlbums(file.PhotoUID, opt.Albums, opt.UID); err != nil {
log.Warn(err) log.Warn(err)
} }
@@ -190,7 +190,7 @@ func ImportWorker(jobs <-chan ImportJob) {
} }
// Index main MediaFile. // Index main MediaFile.
res := ind.UserMediaFile(f, o, originalName, "", opt.UserUID) res := ind.UserMediaFile(f, o, originalName, "", opt.UID)
// Log result. // Log result.
log.Infof("import: %s main %s file %s", res, f.FileType(), clean.Log(f.RootRelName())) log.Infof("import: %s main %s file %s", res, f.FileType(), clean.Log(f.RootRelName()))
@@ -203,7 +203,7 @@ func ImportWorker(jobs <-chan ImportJob) {
photoUID = res.PhotoUID photoUID = res.PhotoUID
// Add photo to album if a list of albums was provided when importing. // Add photo to album if a list of albums was provided when importing.
if err := entity.AddPhotoToUserAlbums(photoUID, opt.Albums, opt.UserUID); err != nil { if err := entity.AddPhotoToUserAlbums(photoUID, opt.Albums, opt.UID); err != nil {
log.Warn(err) log.Warn(err)
} }
} }
@@ -240,7 +240,7 @@ func ImportWorker(jobs <-chan ImportJob) {
} }
// Index related media file including its original filename. // Index related media file including its original filename.
res := ind.UserMediaFile(f, o, relatedOriginalNames[f.FileName()], photoUID, opt.UserUID) res := ind.UserMediaFile(f, o, relatedOriginalNames[f.FileName()], photoUID, opt.UID)
// Save file error. // Save file error.
if fileUid, err := res.FileError(); err != nil { if fileUid, err := res.FileError(); err != nil {

View File

@@ -177,6 +177,7 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
} }
event.Publish("index.folder", event.Data{ event.Publish("index.folder", event.Data{
"uid": o.UID,
"filePath": relName, "filePath": relName,
}) })
@@ -291,6 +292,7 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
if updated > 0 { if updated > 0 {
event.Publish("index.updating", event.Data{ event.Publish("index.updating", event.Data{
"uid": o.UID,
"step": "faces", "step": "faces",
}) })
@@ -302,6 +304,7 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
} }
event.Publish("index.updating", event.Data{ event.Publish("index.updating", event.Data{
"uid": o.UID,
"step": "counts", "step": "counts",
}) })

View File

@@ -76,6 +76,8 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
photoExists := false photoExists := false
event.Publish("index.indexing", event.Data{ event.Publish("index.indexing", event.Data{
"uid": o.UID,
"action": o.Action,
"fileHash": fileHash, "fileHash": fileHash,
"fileSize": fileSize, "fileSize": fileSize,
"fileName": fileName, "fileName": fileName,

View File

@@ -1,7 +1,11 @@
package photoprism package photoprism
import "github.com/photoprism/photoprism/internal/entity"
// IndexOptions represents file indexing options. // IndexOptions represents file indexing options.
type IndexOptions struct { type IndexOptions struct {
UID string
Action string
Path string Path string
Rescan bool Rescan bool
Convert bool Convert bool
@@ -15,6 +19,8 @@ type IndexOptions struct {
// NewIndexOptions returns new index options instance. // NewIndexOptions returns new index options instance.
func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchived bool) IndexOptions { func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchived bool) IndexOptions {
result := IndexOptions{ result := IndexOptions{
UID: entity.Admin.UID(),
Action: ActionIndex,
Path: path, Path: path,
Rescan: rescan, Rescan: rescan,
Convert: convert, Convert: convert,
@@ -33,6 +39,15 @@ func (o *IndexOptions) SkipUnchanged() bool {
return !o.Rescan return !o.Rescan
} }
// SetUser sets the user who performs the index operation.
func (o *IndexOptions) SetUser(user *entity.User) *IndexOptions {
if o != nil && user != nil {
o.UID = user.UID()
}
return o
}
// IndexOptionsAll returns new index options with all options set to true. // IndexOptionsAll returns new index options with all options set to true.
func IndexOptionsAll() IndexOptions { func IndexOptionsAll() IndexOptions {
return NewIndexOptions("/", true, true, true, false, true) return NewIndexOptions("/", true, true, true, false, true)