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.
if s.UserUID != "" {
opt.UserUID = s.UserUID
opt.UID = s.UserUID
}
// Start import.
@ -138,8 +138,16 @@ func StartImport(router *gin.RouterGroup) {
msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed)
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 {
PublishAlbumEvent(EntityUpdated, uid, c)

View file

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

View file

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

View file

@ -41,7 +41,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
// Users may only change their own avatar.
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)
return
}

View file

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

View file

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

View file

@ -69,6 +69,7 @@ func Import() error {
event.InfoMsg(i18n.MsgCopyingFilesFrom, clean.Log(filepath.Base(path)))
var opt photoprism.ImportOptions
opt.Action = photoprism.ActionAutoImport
if conf.Settings().Import.Move {
opt = photoprism.ImportOptionsMove(path, conf.ImportDest())
@ -93,8 +94,16 @@ func Import() error {
msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed)
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()

View file

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

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()
convert := settings.Index.Convert && imp.conf.SidecarWritable()
indexOpt := NewIndexOptions("/", true, convert, true, false, false)
indexOpt.UID = opt.UID
indexOpt.Action = opt.Action
skipRaw := imp.conf.DisableRaw()
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)

View file

@ -1,21 +1,35 @@
package photoprism
import "github.com/photoprism/photoprism/internal/entity"
// ImportOptions represents file import options.
type ImportOptions struct {
UID string
Action string
Albums []string
Path string
Move bool
NonBlocking bool
UserUID string
DestFolder string
RemoveDotFiles bool
RemoveExistingFiles 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).
func ImportOptionsCopy(importPath, destFolder string) ImportOptions {
result := ImportOptions{
UID: entity.Admin.UID(),
Action: ActionImport,
Path: importPath,
Move: 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).
func ImportOptionsMove(importPath, destFolder string) ImportOptions {
result := ImportOptions{
UID: entity.Admin.UID(),
Action: ActionImport,
Path: importPath,
Move: true,
NonBlocking: false,
@ -46,6 +62,8 @@ func ImportOptionsMove(importPath, destFolder string) ImportOptions {
// ImportOptionsUpload returns options for importing user uploads.
func ImportOptionsUpload(uploadPath, destFolder string) ImportOptions {
result := ImportOptions{
UID: entity.Admin.UID(),
Action: ActionUpload,
Path: uploadPath,
Move: true,
NonBlocking: true,

View file

@ -110,7 +110,7 @@ func ImportWorker(jobs <-chan ImportJob) {
// Do nothing.
} else if file, err := entity.FirstFileByHash(fileHash); err != nil {
// 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)
}
@ -190,7 +190,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
// Index main MediaFile.
res := ind.UserMediaFile(f, o, originalName, "", opt.UserUID)
res := ind.UserMediaFile(f, o, originalName, "", opt.UID)
// Log result.
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
// 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)
}
}
@ -240,7 +240,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
// 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.
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{
"uid": o.UID,
"filePath": relName,
})
@ -291,6 +292,7 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
if updated > 0 {
event.Publish("index.updating", event.Data{
"uid": o.UID,
"step": "faces",
})
@ -302,6 +304,7 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
}
event.Publish("index.updating", event.Data{
"uid": o.UID,
"step": "counts",
})

View file

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

View file

@ -1,7 +1,11 @@
package photoprism
import "github.com/photoprism/photoprism/internal/entity"
// IndexOptions represents file indexing options.
type IndexOptions struct {
UID string
Action string
Path string
Rescan bool
Convert bool
@ -15,6 +19,8 @@ type IndexOptions struct {
// NewIndexOptions returns new index options instance.
func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchived bool) IndexOptions {
result := IndexOptions{
UID: entity.Admin.UID(),
Action: ActionIndex,
Path: path,
Rescan: rescan,
Convert: convert,
@ -33,6 +39,15 @@ func (o *IndexOptions) SkipUnchanged() bool {
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.
func IndexOptionsAll() IndexOptions {
return NewIndexOptions("/", true, true, true, false, true)