Change routes for scan-mailbox task to allow multiple tasks per user

This commit is contained in:
Eike Kettner 2020-05-21 09:00:45 +02:00
parent 743aa9d754
commit 9f9dd6c0fb
6 changed files with 213 additions and 59 deletions

View File

@ -2,8 +2,10 @@ package docspell.backend.ops
import cats.implicits._ import cats.implicits._
import cats.effect._ import cats.effect._
import cats.data.OptionT
import com.github.eikek.calev.CalEvent import com.github.eikek.calev.CalEvent
import io.circe.Encoder import io.circe.Encoder
import fs2.Stream
import docspell.store.queue.JobQueue import docspell.store.queue.JobQueue
import docspell.store.usertask._ import docspell.store.usertask._
@ -11,10 +13,15 @@ import docspell.common._
trait OUserTask[F[_]] { trait OUserTask[F[_]] {
/** Return the settings for the scan-mailbox task of the current user. /** Return the settings for all scan-mailbox tasks of the current user.
* There is at most one such task per user.
*/ */
def getScanMailbox(account: AccountId): F[UserTask[ScanMailboxArgs]] def getScanMailbox(account: AccountId): Stream[F, UserTask[ScanMailboxArgs]]
/** Find a scan-mailbox task by the given id. */
def findScanMailbox(
id: Ident,
account: AccountId
): OptionT[F, UserTask[ScanMailboxArgs]]
/** Updates the scan-mailbox tasks and notifies the joex nodes. /** Updates the scan-mailbox tasks and notifies the joex nodes.
*/ */
@ -24,7 +31,9 @@ trait OUserTask[F[_]] {
): F[Unit] ): F[Unit]
/** Return the settings for the notify-due-items task of the current /** Return the settings for the notify-due-items task of the current
* user. There is at most one such task per user. * user. There is at most one such task per user. If no task has
* been created/submitted a new one with default values is
* returned.
*/ */
def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]] def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]]
@ -35,6 +44,9 @@ trait OUserTask[F[_]] {
task: UserTask[NotifyDueItemsArgs] task: UserTask[NotifyDueItemsArgs]
): F[Unit] ): F[Unit]
/** Removes a user task with the given id. */
def deleteTask(account: AccountId, id: Ident): F[Unit]
/** Discards the schedule and immediately submits the task to the job /** Discards the schedule and immediately submits the task to the job
* executor's queue. It will not update the corresponding periodic * executor's queue. It will not update the corresponding periodic
* task. * task.
@ -63,17 +75,28 @@ object OUserTask {
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
def getScanMailbox(account: AccountId): F[UserTask[ScanMailboxArgs]] = def getScanMailbox(account: AccountId): Stream[F, UserTask[ScanMailboxArgs]] =
store store
.getOneByName[ScanMailboxArgs](account, ScanMailboxArgs.taskName) .getByName[ScanMailboxArgs](account, ScanMailboxArgs.taskName)
.getOrElseF(scanMailboxDefault(account))
def findScanMailbox(
id: Ident,
account: AccountId
): OptionT[F, UserTask[ScanMailboxArgs]] =
OptionT(getScanMailbox(account).find(_.id == id).compile.last)
def deleteTask(account: AccountId, id: Ident): F[Unit] =
(for {
_ <- store.getByIdRaw(account, id)
_ <- OptionT.liftF(store.deleteTask(account, id))
} yield ()).getOrElse(())
def submitScanMailbox( def submitScanMailbox(
account: AccountId, account: AccountId,
task: UserTask[ScanMailboxArgs] task: UserTask[ScanMailboxArgs]
): F[Unit] = ): F[Unit] =
for { for {
_ <- store.updateOneTask[ScanMailboxArgs](account, task) _ <- store.updateTask[ScanMailboxArgs](account, task)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
@ -113,26 +136,26 @@ object OUserTask {
) )
) )
private def scanMailboxDefault( // private def scanMailboxDefault(
account: AccountId // account: AccountId
): F[UserTask[ScanMailboxArgs]] = // ): F[UserTask[ScanMailboxArgs]] =
for { // for {
id <- Ident.randomId[F] // id <- Ident.randomId[F]
} yield UserTask( // } yield UserTask(
id, // id,
ScanMailboxArgs.taskName, // ScanMailboxArgs.taskName,
false, // false,
CalEvent.unsafe("*-*-* 0,12:00"), // CalEvent.unsafe("*-*-* 0,12:00"),
ScanMailboxArgs( // ScanMailboxArgs(
account, // account,
Ident.unsafe(""), // Ident.unsafe(""),
Nil, // Nil,
Some(Duration.hours(12)), // Some(Duration.hours(12)),
None, // None,
false, // false,
None // None
) // )
) // )
}) })
} }

View File

@ -9,7 +9,13 @@ import cats.effect.Sync
import io.circe.{Decoder, Encoder} import io.circe.{Decoder, Encoder}
import scodec.bits.ByteVector import scodec.bits.ByteVector
case class Ident(id: String) {} case class Ident(id: String) {
def isEmpty: Boolean =
id.trim.isEmpty
def nonEmpty: Boolean =
!isEmpty
}
object Ident { object Ident {
implicit val identEq: Eq[Ident] = implicit val identEq: Eq[Ident] =

View File

@ -1760,9 +1760,10 @@ paths:
tags: [ User Tasks ] tags: [ User Tasks ]
summary: Get settings for "Scan Mailbox" task summary: Get settings for "Scan Mailbox" task
description: | description: |
Return the current settings for the scan mailbox task of the Return the current settings for the scan-mailbox tasks of the
authenticated user. Users can periodically fetch mails to be authenticated user. Users can periodically fetch mails to be
imported into docspell. imported into docspell. It is possible to have multiple of
these tasks.
security: security:
- authTokenHeader: [] - authTokenHeader: []
responses: responses:
@ -1771,13 +1772,13 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/ScanMailboxSettings" $ref: "#/components/schemas/ScanMailboxSettingsList"
post: post:
tags: [ User Tasks ] tags: [ User Tasks ]
summary: Change current settings for "Scan Mailbox" task summary: Create settings for "Scan Mailbox" task
description: | description: |
Change the current settings for the scan-mailbox task of the Create new settings for a scan-mailbox task. The id field in
authenticated user. the input data is ignored.
security: security:
- authTokenHeader: [] - authTokenHeader: []
requestBody: requestBody:
@ -1792,6 +1793,61 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/BasicResult" $ref: "#/components/schemas/BasicResult"
put:
tags: [ User Tasks ]
summary: Change current settings for "Scan Mailbox" task
description: |
Change the settings for a scan-mailbox task. The task is
looked up by its id.
security:
- authTokenHeader: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ScanMailboxSettings"
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/usertask/scanmailbox/{id}:
parameters:
- $ref: "#/components/parameters/id"
get:
tags: [ User Tasks ]
summary: Get settings for "Scan Mailbox" task
description: |
Return the current settings for a single scan-mailbox task of
the authenticated user. Users can periodically fetch mails to
be imported into docspell.
security:
- authTokenHeader: []
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/ScanMailboxSettings"
delete:
tags: [ User Tasks ]
summary: Delete a scan-mailbox task.
description: |
Deletes the settings to a scan-mailbox task of the
authenticated user.
security:
- authTokenHeader: []
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/usertask/scanmailbox/startonce: /sec/usertask/scanmailbox/startonce:
post: post:
tags: [ User Tasks ] tags: [ User Tasks ]
@ -1816,6 +1872,16 @@ paths:
components: components:
schemas: schemas:
ScanMailboxSettingsList:
description: |
A list of scan-mailbox tasks.
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/ScanMailboxSettings"
ScanMailboxSettings: ScanMailboxSettings:
description: | description: |
Settings for the scan mailbox task. Settings for the scan mailbox task.

View File

@ -2,6 +2,7 @@ package docspell.restserver.routes
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import cats.data.OptionT
import org.http4s._ import org.http4s._
import org.http4s.dsl.Http4sDsl import org.http4s.dsl.Http4sDsl
import org.http4s.circe.CirceEntityEncoder._ import org.http4s.circe.CirceEntityEncoder._
@ -25,10 +26,18 @@ object ScanMailboxRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root / Ident(id) =>
(for {
task <- ut.findScanMailbox(id, user.account)
res <- OptionT.liftF(taskToSettings(user.account, backend, task))
resp <- OptionT.liftF(Ok(res))
} yield resp).getOrElseF(NotFound())
case req @ POST -> Root / "startonce" => case req @ POST -> Root / "startonce" =>
for { for {
data <- req.as[ScanMailboxSettings] data <- req.as[ScanMailboxSettings]
task = makeTask(user.account, data) newId <- Ident.randomId[F]
task <- makeTask(newId, user.account, data)
res <- res <-
ut.executeNow(user.account, task) ut.executeNow(user.account, task)
.attempt .attempt
@ -36,43 +45,74 @@ object ScanMailboxRoutes {
resp <- Ok(res) resp <- Ok(res)
} yield resp } yield resp
case GET -> Root => case DELETE -> Root / Ident(id) =>
for { for {
task <- ut.getScanMailbox(user.account) res <-
res <- taskToSettings(user.account, backend, task) ut.deleteTask(user.account, id)
.attempt
.map(Conversions.basicResult(_, "Deleted successfully."))
resp <- Ok(res) resp <- Ok(res)
} yield resp } yield resp
case req @ PUT -> Root =>
def run(data: ScanMailboxSettings) =
for {
task <- makeTask(data.id, user.account, data)
res <-
ut.submitScanMailbox(user.account, task)
.attempt
.map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res)
} yield resp
for {
data <- req.as[ScanMailboxSettings]
resp <-
if (data.id.isEmpty) Ok(BasicResult(false, "Empty id is not allowed"))
else run(data)
} yield resp
case req @ POST -> Root => case req @ POST -> Root =>
for { for {
data <- req.as[ScanMailboxSettings] data <- req.as[ScanMailboxSettings]
task = makeTask(user.account, data) newId <- Ident.randomId[F]
task <- makeTask(newId, user.account, data)
res <- res <-
ut.submitScanMailbox(user.account, task) ut.submitScanMailbox(user.account, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Saved successfully.")) .map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res) resp <- Ok(res)
} yield resp } yield resp
case GET -> Root =>
ut.getScanMailbox(user.account)
.evalMap(task => taskToSettings(user.account, backend, task))
.compile
.toVector
.map(v => ScanMailboxSettingsList(v.toList))
.flatMap(Ok(_))
} }
} }
def makeTask( def makeTask[F[_]: Sync](
id: Ident,
user: AccountId, user: AccountId,
settings: ScanMailboxSettings settings: ScanMailboxSettings
): UserTask[ScanMailboxArgs] = ): F[UserTask[ScanMailboxArgs]] =
UserTask( Sync[F].pure(
settings.id, UserTask(
ScanMailboxArgs.taskName, id,
settings.enabled, ScanMailboxArgs.taskName,
settings.schedule, settings.enabled,
ScanMailboxArgs( settings.schedule,
user, ScanMailboxArgs(
settings.imapConnection, user,
settings.folders, settings.imapConnection,
settings.receivedSinceHours.map(_.toLong).map(Duration.hours), settings.folders,
settings.targetFolder, settings.receivedSinceHours.map(_.toLong).map(Duration.hours),
settings.deleteMail, settings.targetFolder,
settings.direction settings.deleteMail,
settings.direction
)
) )
) )

View File

@ -31,6 +31,20 @@ object QUserTask {
) )
).query[RPeriodicTask].stream.map(makeUserTask) ).query[RPeriodicTask].stream.map(makeUserTask)
def findById(
account: AccountId,
id: Ident
): ConnectionIO[Option[UserTask[String]]] =
selectSimple(
RPeriodicTask.Columns.all,
RPeriodicTask.table,
and(
cols.group.is(account.collective),
cols.submitter.is(account.user),
cols.id.is(id)
)
).query[RPeriodicTask].option.map(_.map(makeUserTask))
def insert(account: AccountId, task: UserTask[String]): ConnectionIO[Int] = def insert(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
for { for {
r <- task.toPeriodicTask[ConnectionIO](account) r <- task.toPeriodicTask[ConnectionIO](account)

View File

@ -42,12 +42,14 @@ trait UserTaskStore[F[_]] {
D: Decoder[A] D: Decoder[A]
): Stream[F, UserTask[A]] ): Stream[F, UserTask[A]]
/** Return a user-task with the given id. */
def getByIdRaw(account: AccountId, id: Ident): OptionT[F, UserTask[String]]
/** Updates or inserts the given task. /** Updates or inserts the given task.
* *
* The task is identified by its id. If no task with this id * The task is identified by its id. If no task with this id
* exists, a new one is created. Otherwise the existing task is * exists, a new one is created. Otherwise the existing task is
* updated. The job executors are notified if a task has been * updated.
* enabled.
*/ */
def updateTask[A](account: AccountId, ut: UserTask[A])(implicit E: Encoder[A]): F[Int] def updateTask[A](account: AccountId, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
@ -100,6 +102,9 @@ object UserTaskStore {
def getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]] = def getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]] =
store.transact(QUserTask.findByName(account, name)) store.transact(QUserTask.findByName(account, name))
def getByIdRaw(account: AccountId, id: Ident): OptionT[F, UserTask[String]] =
OptionT(store.transact(QUserTask.findById(account, id)))
def getByName[A](account: AccountId, name: Ident)(implicit def getByName[A](account: AccountId, name: Ident)(implicit
D: Decoder[A] D: Decoder[A]
): Stream[F, UserTask[A]] = ): Stream[F, UserTask[A]] =