diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala index 26233eb1..21b916ab 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala @@ -2,8 +2,10 @@ package docspell.backend.ops import cats.implicits._ import cats.effect._ +import cats.data.OptionT import com.github.eikek.calev.CalEvent import io.circe.Encoder +import fs2.Stream import docspell.store.queue.JobQueue import docspell.store.usertask._ @@ -11,10 +13,15 @@ import docspell.common._ trait OUserTask[F[_]] { - /** Return the settings for the scan-mailbox task of the current user. - * There is at most one such task per user. + /** Return the settings for all scan-mailbox tasks of the current 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. */ @@ -24,7 +31,9 @@ trait OUserTask[F[_]] { ): F[Unit] /** 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]] @@ -35,6 +44,9 @@ trait OUserTask[F[_]] { task: UserTask[NotifyDueItemsArgs] ): 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 * executor's queue. It will not update the corresponding periodic * task. @@ -63,17 +75,28 @@ object OUserTask { _ <- joex.notifyAllNodes } yield () - def getScanMailbox(account: AccountId): F[UserTask[ScanMailboxArgs]] = + def getScanMailbox(account: AccountId): Stream[F, UserTask[ScanMailboxArgs]] = store - .getOneByName[ScanMailboxArgs](account, ScanMailboxArgs.taskName) - .getOrElseF(scanMailboxDefault(account)) + .getByName[ScanMailboxArgs](account, ScanMailboxArgs.taskName) + + 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( account: AccountId, task: UserTask[ScanMailboxArgs] ): F[Unit] = for { - _ <- store.updateOneTask[ScanMailboxArgs](account, task) + _ <- store.updateTask[ScanMailboxArgs](account, task) _ <- joex.notifyAllNodes } yield () @@ -113,26 +136,26 @@ object OUserTask { ) ) - private def scanMailboxDefault( - account: AccountId - ): F[UserTask[ScanMailboxArgs]] = - for { - id <- Ident.randomId[F] - } yield UserTask( - id, - ScanMailboxArgs.taskName, - false, - CalEvent.unsafe("*-*-* 0,12:00"), - ScanMailboxArgs( - account, - Ident.unsafe(""), - Nil, - Some(Duration.hours(12)), - None, - false, - None - ) - ) + // private def scanMailboxDefault( + // account: AccountId + // ): F[UserTask[ScanMailboxArgs]] = + // for { + // id <- Ident.randomId[F] + // } yield UserTask( + // id, + // ScanMailboxArgs.taskName, + // false, + // CalEvent.unsafe("*-*-* 0,12:00"), + // ScanMailboxArgs( + // account, + // Ident.unsafe(""), + // Nil, + // Some(Duration.hours(12)), + // None, + // false, + // None + // ) + // ) }) } diff --git a/modules/common/src/main/scala/docspell/common/Ident.scala b/modules/common/src/main/scala/docspell/common/Ident.scala index 49577fac..a1d6cb8a 100644 --- a/modules/common/src/main/scala/docspell/common/Ident.scala +++ b/modules/common/src/main/scala/docspell/common/Ident.scala @@ -9,7 +9,13 @@ import cats.effect.Sync import io.circe.{Decoder, Encoder} 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 { implicit val identEq: Eq[Ident] = diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 6c4a4103..dce7205a 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1760,9 +1760,10 @@ paths: tags: [ User Tasks ] summary: Get settings for "Scan Mailbox" task 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 - imported into docspell. + imported into docspell. It is possible to have multiple of + these tasks. security: - authTokenHeader: [] responses: @@ -1771,13 +1772,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ScanMailboxSettings" + $ref: "#/components/schemas/ScanMailboxSettingsList" post: tags: [ User Tasks ] - summary: Change current settings for "Scan Mailbox" task + summary: Create settings for "Scan Mailbox" task description: | - Change the current settings for the scan-mailbox task of the - authenticated user. + Create new settings for a scan-mailbox task. The id field in + the input data is ignored. security: - authTokenHeader: [] requestBody: @@ -1792,6 +1793,61 @@ paths: application/json: schema: $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: post: tags: [ User Tasks ] @@ -1816,6 +1872,16 @@ paths: components: schemas: + ScanMailboxSettingsList: + description: | + A list of scan-mailbox tasks. + required: + - items + properties: + items: + type: array + items: + $ref: "#/components/schemas/ScanMailboxSettings" ScanMailboxSettings: description: | Settings for the scan mailbox task. diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala index 5c7d0fa5..f5ab3479 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala @@ -2,6 +2,7 @@ package docspell.restserver.routes import cats.effect._ import cats.implicits._ +import cats.data.OptionT import org.http4s._ import org.http4s.dsl.Http4sDsl import org.http4s.circe.CirceEntityEncoder._ @@ -25,10 +26,18 @@ object ScanMailboxRoutes { import dsl._ 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" => for { - data <- req.as[ScanMailboxSettings] - task = makeTask(user.account, data) + data <- req.as[ScanMailboxSettings] + newId <- Ident.randomId[F] + task <- makeTask(newId, user.account, data) res <- ut.executeNow(user.account, task) .attempt @@ -36,43 +45,74 @@ object ScanMailboxRoutes { resp <- Ok(res) } yield resp - case GET -> Root => + case DELETE -> Root / Ident(id) => for { - task <- ut.getScanMailbox(user.account) - res <- taskToSettings(user.account, backend, task) + res <- + ut.deleteTask(user.account, id) + .attempt + .map(Conversions.basicResult(_, "Deleted successfully.")) resp <- Ok(res) } 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 => for { - data <- req.as[ScanMailboxSettings] - task = makeTask(user.account, data) + data <- req.as[ScanMailboxSettings] + newId <- Ident.randomId[F] + task <- makeTask(newId, user.account, data) res <- ut.submitScanMailbox(user.account, task) .attempt .map(Conversions.basicResult(_, "Saved successfully.")) resp <- Ok(res) } 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, settings: ScanMailboxSettings - ): UserTask[ScanMailboxArgs] = - UserTask( - settings.id, - ScanMailboxArgs.taskName, - settings.enabled, - settings.schedule, - ScanMailboxArgs( - user, - settings.imapConnection, - settings.folders, - settings.receivedSinceHours.map(_.toLong).map(Duration.hours), - settings.targetFolder, - settings.deleteMail, - settings.direction + ): F[UserTask[ScanMailboxArgs]] = + Sync[F].pure( + UserTask( + id, + ScanMailboxArgs.taskName, + settings.enabled, + settings.schedule, + ScanMailboxArgs( + user, + settings.imapConnection, + settings.folders, + settings.receivedSinceHours.map(_.toLong).map(Duration.hours), + settings.targetFolder, + settings.deleteMail, + settings.direction + ) ) ) diff --git a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala index b64d24f1..41c9457f 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala @@ -31,6 +31,20 @@ object QUserTask { ) ).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] = for { r <- task.toPeriodicTask[ConnectionIO](account) diff --git a/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala b/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala index 3287c5ec..d1e9690c 100644 --- a/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala +++ b/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala @@ -42,12 +42,14 @@ trait UserTaskStore[F[_]] { D: Decoder[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. * * The task is identified by its id. If no task with this id * exists, a new one is created. Otherwise the existing task is - * updated. The job executors are notified if a task has been - * enabled. + * updated. */ 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]] = 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 D: Decoder[A] ): Stream[F, UserTask[A]] =