Implement content extraction job (#18022)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
758116f3e3
Коммит
a5463c8651
@@ -139,6 +139,12 @@ func RegisterJobsExportDeleteInterface(f func(*Server) tjobs.ExportDeleteInterfa
|
|||||||
jobsExportDeleteInterface = f
|
jobsExportDeleteInterface = f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jobsExtractContentInterface func(*Server) tjobs.ExtractContentInterface
|
||||||
|
|
||||||
|
func RegisterJobsExtractContentInterface(f func(*Server) tjobs.ExtractContentInterface) {
|
||||||
|
jobsExtractContentInterface = f
|
||||||
|
}
|
||||||
|
|
||||||
var productNoticesJobInterface func(*Server) tjobs.ProductNoticesJobInterface
|
var productNoticesJobInterface func(*Server) tjobs.ProductNoticesJobInterface
|
||||||
|
|
||||||
func RegisterProductNoticesJobInterface(f func(*Server) tjobs.ProductNoticesJobInterface) {
|
func RegisterProductNoticesJobInterface(f func(*Server) tjobs.ProductNoticesJobInterface) {
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ func (a *App) SessionHasPermissionToCreateJob(session model.Session, job *model.
|
|||||||
model.JobTypeImportDelete,
|
model.JobTypeImportDelete,
|
||||||
model.JobTypeExportProcess,
|
model.JobTypeExportProcess,
|
||||||
model.JobTypeExportDelete,
|
model.JobTypeExportDelete,
|
||||||
model.JobTypeCloud:
|
model.JobTypeCloud,
|
||||||
|
model.JobTypeExtractContent:
|
||||||
return a.SessionHasPermissionTo(session, model.PermissionManageJobs), model.PermissionManageJobs
|
return a.SessionHasPermissionTo(session, model.PermissionManageJobs), model.PermissionManageJobs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +127,8 @@ func (a *App) SessionHasPermissionToReadJob(session model.Session, jobType strin
|
|||||||
model.JobTypeImportDelete,
|
model.JobTypeImportDelete,
|
||||||
model.JobTypeExportProcess,
|
model.JobTypeExportProcess,
|
||||||
model.JobTypeExportDelete,
|
model.JobTypeExportDelete,
|
||||||
model.JobTypeCloud:
|
model.JobTypeCloud,
|
||||||
|
model.JobTypeExtractContent:
|
||||||
return a.SessionHasPermissionTo(session, model.PermissionReadJobs), model.PermissionReadJobs
|
return a.SessionHasPermissionTo(session, model.PermissionReadJobs), model.PermissionReadJobs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2061,6 +2061,10 @@ func (s *Server) initJobs() {
|
|||||||
s.Jobs.ResendInvitationEmails = jobsResendInvitationEmailInterface(s)
|
s.Jobs.ResendInvitationEmails = jobsResendInvitationEmailInterface(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jobsExtractContentInterface != nil {
|
||||||
|
s.Jobs.ExtractContent = jobsExtractContentInterface(s)
|
||||||
|
}
|
||||||
|
|
||||||
s.Jobs.InitWorkers()
|
s.Jobs.InitWorkers()
|
||||||
s.Jobs.InitSchedulers()
|
s.Jobs.InitSchedulers()
|
||||||
}
|
}
|
||||||
|
|||||||
12
i18n/en.json
12
i18n/en.json
@@ -7494,6 +7494,18 @@
|
|||||||
"id": "error",
|
"id": "error",
|
||||||
"translation": "Error"
|
"translation": "Error"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "extrac_content.worker.do_job.invalid_input.from",
|
||||||
|
"translation": "Invalid input value 'from'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "extrac_content.worker.do_job.invalid_input.to",
|
||||||
|
"translation": "Invalid input value 'to'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "extract_content.worker.do_job.file_info",
|
||||||
|
"translation": "Failed to get file information for content extraction."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "group_not_associated_to_synced_team",
|
"id": "group_not_associated_to_synced_team",
|
||||||
"translation": "Group cannot be associated to the channel until it is first associated to the parent group-synced team."
|
"translation": "Group cannot be associated to the channel until it is first associated to the parent group-synced team."
|
||||||
|
|||||||
@@ -36,4 +36,7 @@ import (
|
|||||||
|
|
||||||
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty.
|
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty.
|
||||||
_ "github.com/mattermost/mattermost-server/v6/jobs/resend_invitation_email"
|
_ "github.com/mattermost/mattermost-server/v6/jobs/resend_invitation_email"
|
||||||
|
|
||||||
|
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty.
|
||||||
|
_ "github.com/mattermost/mattermost-server/v6/jobs/extract_content"
|
||||||
)
|
)
|
||||||
|
|||||||
176
jobs/extract_content/worker.go
Обычный файл
176
jobs/extract_content/worker.go
Обычный файл
@@ -0,0 +1,176 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package extract_content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/app"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/jobs"
|
||||||
|
tjobs "github.com/mattermost/mattermost-server/v6/jobs/interfaces"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ignoredFiles = map[string]bool{
|
||||||
|
"png": true, "jpg": true, "jpeg": true, "gif": true, "wmv": true,
|
||||||
|
"mpg": true, "mpeg": true, "mp3": true, "mp4": true, "ogg": true,
|
||||||
|
"ogv": true, "mov": true, "apk": true, "svg": true, "webm": true,
|
||||||
|
"mkv": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
app.RegisterJobsExtractContentInterface(func(s *app.Server) tjobs.ExtractContentInterface {
|
||||||
|
a := app.New(app.ServerConnector(s))
|
||||||
|
return &ExtractContentInterfaceImpl{a}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtractContentInterfaceImpl struct {
|
||||||
|
app *app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtractContentWorker struct {
|
||||||
|
name string
|
||||||
|
stopChan chan struct{}
|
||||||
|
stoppedChan chan struct{}
|
||||||
|
jobsChan chan model.Job
|
||||||
|
jobServer *jobs.JobServer
|
||||||
|
app *app.App
|
||||||
|
appContext *request.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ExtractContentInterfaceImpl) MakeWorker() model.Worker {
|
||||||
|
return &ExtractContentWorker{
|
||||||
|
name: "ExtractContent",
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
stoppedChan: make(chan struct{}),
|
||||||
|
jobsChan: make(chan model.Job),
|
||||||
|
jobServer: i.app.Srv().Jobs,
|
||||||
|
app: i.app,
|
||||||
|
appContext: &request.Context{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) JobChannel() chan<- model.Job {
|
||||||
|
return w.jobsChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) Run() {
|
||||||
|
mlog.Debug("Worker started", mlog.String("worker", w.name))
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
mlog.Debug("Worker finished", mlog.String("worker", w.name))
|
||||||
|
close(w.stoppedChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.stopChan:
|
||||||
|
mlog.Debug("Worker received stop signal", mlog.String("worker", w.name))
|
||||||
|
return
|
||||||
|
case job := <-w.jobsChan:
|
||||||
|
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", w.name))
|
||||||
|
w.doJob(&job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) Stop() {
|
||||||
|
mlog.Debug("Worker stopping", mlog.String("worker", w.name))
|
||||||
|
close(w.stopChan)
|
||||||
|
<-w.stoppedChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) doJob(job *model.Job) {
|
||||||
|
if claimed, err := w.jobServer.ClaimJob(job); err != nil {
|
||||||
|
mlog.Warn("Worker experienced an error while trying to claim job",
|
||||||
|
mlog.String("worker", w.name),
|
||||||
|
mlog.String("job_id", job.Id),
|
||||||
|
mlog.String("error", err.Error()))
|
||||||
|
return
|
||||||
|
} else if !claimed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var fromTS int64 = 0
|
||||||
|
var toTS int64 = model.GetMillis()
|
||||||
|
if fromStr, ok := job.Data["from"]; ok {
|
||||||
|
if fromTS, err = strconv.ParseInt(fromStr, 10, 64); err != nil {
|
||||||
|
w.setJobError(job, model.NewAppError("ExtractContentWorker", "extrac_content.worker.do_job.invalid_input.from", nil, "", http.StatusBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fromTS *= 1000
|
||||||
|
}
|
||||||
|
if toStr, ok := job.Data["to"]; ok {
|
||||||
|
if toTS, err = strconv.ParseInt(toStr, 10, 64); err != nil {
|
||||||
|
w.setJobError(job, model.NewAppError("ExtractContentWorker", "extrac_content.worker.do_job.invalid_input.to", nil, "", http.StatusBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toTS *= 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
var nFiles int
|
||||||
|
var nErrs int
|
||||||
|
for {
|
||||||
|
opts := model.GetFileInfosOptions{
|
||||||
|
Since: fromTS,
|
||||||
|
SortBy: model.FileinfoSortByCreated,
|
||||||
|
IncludeDeleted: false,
|
||||||
|
}
|
||||||
|
fileInfos, err := w.app.Srv().Store.FileInfo().GetWithOptions(0, 1000, &opts)
|
||||||
|
if err != nil {
|
||||||
|
w.setJobError(job, model.NewAppError("ExtractContentWorker", "extract_content.worker.do_job.file_info", nil, err.Error(), http.StatusInternalServerError))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(fileInfos) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, fileInfo := range fileInfos {
|
||||||
|
if !ignoredFiles[fileInfo.Extension] {
|
||||||
|
mlog.Debug("extracting file", mlog.String("filename", fileInfo.Name), mlog.String("filepath", fileInfo.Path))
|
||||||
|
err = w.app.ExtractContentFromFileInfo(fileInfo)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Warn("Failed to extract file content", mlog.Err(err), mlog.String("file_info_id", fileInfo.Id))
|
||||||
|
nErrs++
|
||||||
|
}
|
||||||
|
nFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastFileInfo := fileInfos[len(fileInfos)-1]
|
||||||
|
if lastFileInfo.CreateAt > toTS {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fromTS = lastFileInfo.CreateAt + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
job.Data["errors"] = strconv.Itoa(nErrs)
|
||||||
|
job.Data["processed"] = strconv.Itoa(nFiles)
|
||||||
|
w.updateData(job)
|
||||||
|
|
||||||
|
mlog.Info("Worker: Job is complete", mlog.String("worker", w.name), mlog.String("job_id", job.Id))
|
||||||
|
w.setJobSuccess(job)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) setJobSuccess(job *model.Job) {
|
||||||
|
if err := w.app.Srv().Jobs.SetJobSuccess(job); err != nil {
|
||||||
|
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", w.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||||
|
w.setJobError(job, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) setJobError(job *model.Job, appError *model.AppError) {
|
||||||
|
if err := w.app.Srv().Jobs.SetJobError(job, appError); err != nil {
|
||||||
|
mlog.Error("Worker: Failed to set job error", mlog.String("worker", w.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExtractContentWorker) updateData(job *model.Job) {
|
||||||
|
if err := w.app.Srv().Jobs.UpdateInProgressJobData(job); err != nil {
|
||||||
|
mlog.Error("Worker: Failed to update job data", mlog.String("worker", w.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
12
jobs/interfaces/extract_content_interface.go
Обычный файл
12
jobs/interfaces/extract_content_interface.go
Обычный файл
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExtractContentInterface interface {
|
||||||
|
MakeWorker() model.Worker
|
||||||
|
}
|
||||||
@@ -191,6 +191,13 @@ func (watcher *Watcher) PollAndNotify() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if job.Type == model.JobTypeExtractContent {
|
||||||
|
if watcher.workers.ExtractContent != nil {
|
||||||
|
select {
|
||||||
|
case watcher.workers.ExtractContent.JobChannel() <- *job:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type JobServer struct {
|
|||||||
ExportDelete tjobs.ExportDeleteInterface
|
ExportDelete tjobs.ExportDeleteInterface
|
||||||
Cloud ejobs.CloudJobInterface
|
Cloud ejobs.CloudJobInterface
|
||||||
ResendInvitationEmails ejobs.ResendInvitationEmailJobInterface
|
ResendInvitationEmails ejobs.ResendInvitationEmailJobInterface
|
||||||
|
ExtractContent tjobs.ExtractContentInterface
|
||||||
|
|
||||||
// mut is used to protect the following fields from concurrent access.
|
// mut is used to protect the following fields from concurrent access.
|
||||||
mut sync.Mutex
|
mut sync.Mutex
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type Workers struct {
|
|||||||
ExportDelete model.Worker
|
ExportDelete model.Worker
|
||||||
Cloud model.Worker
|
Cloud model.Worker
|
||||||
ResendInvitationEmail model.Worker
|
ResendInvitationEmail model.Worker
|
||||||
|
ExtractContent model.Worker
|
||||||
|
|
||||||
listenerId string
|
listenerId string
|
||||||
running bool
|
running bool
|
||||||
@@ -124,6 +125,10 @@ func (srv *JobServer) InitWorkers() error {
|
|||||||
workers.ResendInvitationEmail = resendInvitationEmailInterface.MakeWorker()
|
workers.ResendInvitationEmail = resendInvitationEmailInterface.MakeWorker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if extractContentInterface := srv.ExtractContent; extractContentInterface != nil {
|
||||||
|
workers.ExtractContent = extractContentInterface.MakeWorker()
|
||||||
|
}
|
||||||
|
|
||||||
srv.workers = workers
|
srv.workers = workers
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -202,6 +207,10 @@ func (workers *Workers) Start() {
|
|||||||
go workers.ResendInvitationEmail.Run()
|
go workers.ResendInvitationEmail.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if workers.ExtractContent != nil {
|
||||||
|
go workers.ExtractContent.Run()
|
||||||
|
}
|
||||||
|
|
||||||
go workers.Watcher.Start()
|
go workers.Watcher.Start()
|
||||||
|
|
||||||
workers.listenerId = workers.ConfigService.AddConfigListener(workers.handleConfigChange)
|
workers.listenerId = workers.ConfigService.AddConfigListener(workers.handleConfigChange)
|
||||||
@@ -335,6 +344,10 @@ func (workers *Workers) Stop() {
|
|||||||
workers.ResendInvitationEmail.Stop()
|
workers.ResendInvitationEmail.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if workers.ExtractContent != nil {
|
||||||
|
workers.ExtractContent.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
workers.running = false
|
workers.running = false
|
||||||
|
|
||||||
mlog.Info("Stopped workers")
|
mlog.Info("Stopped workers")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const (
|
|||||||
JobTypeExportDelete = "export_delete"
|
JobTypeExportDelete = "export_delete"
|
||||||
JobTypeCloud = "cloud"
|
JobTypeCloud = "cloud"
|
||||||
JobTypeResendInvitationEmail = "resend_invitation_email"
|
JobTypeResendInvitationEmail = "resend_invitation_email"
|
||||||
|
JobTypeExtractContent = "extract_content"
|
||||||
|
|
||||||
JobStatusPending = "pending"
|
JobStatusPending = "pending"
|
||||||
JobStatusInProgress = "in_progress"
|
JobStatusInProgress = "in_progress"
|
||||||
@@ -55,6 +56,7 @@ var AllJobTypes = [...]string{
|
|||||||
JobTypeExportProcess,
|
JobTypeExportProcess,
|
||||||
JobTypeExportDelete,
|
JobTypeExportDelete,
|
||||||
JobTypeCloud,
|
JobTypeCloud,
|
||||||
|
JobTypeExtractContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
@@ -96,6 +98,7 @@ func (j *Job) IsValid() *AppError {
|
|||||||
case JobTypeExportDelete:
|
case JobTypeExportDelete:
|
||||||
case JobTypeCloud:
|
case JobTypeCloud:
|
||||||
case JobTypeResendInvitationEmail:
|
case JobTypeResendInvitationEmail:
|
||||||
|
case JobTypeExtractContent:
|
||||||
default:
|
default:
|
||||||
return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest)
|
return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user