From 07e9a9767e15d2781247e42fc466d28644da9a7f Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Wed, 12 Aug 2020 22:26:44 +0200 Subject: [PATCH 1/6] Add a task to re-process files of an item --- .../scala/docspell/backend/JobFactory.scala | 22 +++ .../scala/docspell/backend/ops/OUpload.scala | 28 ++++ .../docspell/common/ReProcessItemArgs.scala | 24 ++++ .../scala/docspell/joex/JoexAppImpl.scala | 8 ++ .../docspell/joex/process/ConvertPdf.scala | 45 +++++- .../docspell/joex/process/ProcessItem.scala | 11 ++ .../docspell/joex/process/ReProcessItem.scala | 131 ++++++++++++++++++ .../src/main/resources/docspell-openapi.yml | 25 ++++ .../restserver/routes/ItemRoutes.scala | 9 ++ .../docspell/store/records/RAttachment.scala | 10 ++ .../store/records/RAttachmentSource.scala | 17 +++ .../docspell/store/records/RCollective.scala | 22 +++ .../scala/docspell/store/records/RItem.scala | 3 + 13 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 modules/common/src/main/scala/docspell/common/ReProcessItemArgs.scala create mode 100644 modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala diff --git a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala index d7d8fe91..96399ffa 100644 --- a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala +++ b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala @@ -8,6 +8,28 @@ import docspell.store.records.RJob object JobFactory { + def reprocessItem[F[_]: Sync]( + args: ReProcessItemArgs, + account: AccountId, + prio: Priority, + tracker: Option[Ident] + ): F[RJob] = + for { + id <- Ident.randomId[F] + now <- Timestamp.current[F] + job = RJob.newJob( + id, + ReProcessItemArgs.taskName, + account.collective, + args, + s"Re-process files of item ${args.itemId.id}", + now, + account.user, + prio, + tracker + ) + } yield job + def processItem[F[_]: Sync]( args: ProcessItemArgs, account: AccountId, diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala index a9145f72..a6fbce49 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala @@ -44,6 +44,19 @@ trait OUpload[F[_]] { case Left(srcId) => submit(data, srcId, notifyJoex, itemId) } + + /** Submits the item for re-processing. The list of attachment ids can + * be used to only re-process a subset of the item's attachments. + * If this list is empty, all attachments are reprocessed. This + * call only submits the job into the queue. + */ + def reprocess( + item: Ident, + attachments: List[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[OUpload.UploadResult] + } object OUpload { @@ -159,6 +172,21 @@ object OUpload { result <- OptionT.liftF(submit(updata, accId, notifyJoex, itemId)) } yield result).getOrElse(UploadResult.noSource) + def reprocess( + item: Ident, + attachments: List[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[UploadResult] = + (for { + _ <- + OptionT(store.transact(RItem.findByIdAndCollective(item, account.collective))) + args = ReProcessItemArgs(item, attachments) + job <- + OptionT.liftF(JobFactory.reprocessItem[F](args, account, Priority.Low, None)) + res <- OptionT.liftF(submitJobs(notifyJoex)(Vector(job))) + } yield res).getOrElse(UploadResult.noItem) + private def submitJobs( notifyJoex: Boolean )(jobs: Vector[RJob]): F[OUpload.UploadResult] = diff --git a/modules/common/src/main/scala/docspell/common/ReProcessItemArgs.scala b/modules/common/src/main/scala/docspell/common/ReProcessItemArgs.scala new file mode 100644 index 00000000..f8afdd58 --- /dev/null +++ b/modules/common/src/main/scala/docspell/common/ReProcessItemArgs.scala @@ -0,0 +1,24 @@ +package docspell.common + +import io.circe.generic.semiauto._ +import io.circe.{Decoder, Encoder} + +/** Arguments when re-processing an item. + * + * The `itemId` must exist and point to some item. If the attachment + * list is non-empty, only those attachments are re-processed. They + * must belong to the given item. If the list is empty, then all + * attachments are re-processed. + */ +case class ReProcessItemArgs(itemId: Ident, attachments: List[Ident]) + +object ReProcessItemArgs { + + val taskName: Ident = Ident.unsafe("re-process-item") + + implicit val jsonEncoder: Encoder[ReProcessItemArgs] = + deriveEncoder[ReProcessItemArgs] + + implicit val jsonDecoder: Decoder[ReProcessItemArgs] = + deriveDecoder[ReProcessItemArgs] +} diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala index 965659b7..9882455c 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala @@ -14,6 +14,7 @@ import docspell.joex.fts.{MigrationTask, ReIndexTask} import docspell.joex.hk._ import docspell.joex.notify._ import docspell.joex.process.ItemHandler +import docspell.joex.process.ReProcessItem import docspell.joex.scanmailbox._ import docspell.joex.scheduler._ import docspell.joexapi.client.JoexClient @@ -96,6 +97,13 @@ object JoexAppImpl { ItemHandler.onCancel[F] ) ) + .withTask( + JobTask.json( + ReProcessItemArgs.taskName, + ReProcessItem[F](cfg, fts), + ReProcessItem.onCancel[F] + ) + ) .withTask( JobTask.json( NotifyDueItemsArgs.taskName, diff --git a/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala b/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala index ba75ec3a..572a18bb 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala @@ -126,11 +126,46 @@ object ConvertPdf { .compile .lastOrError .map(fm => Ident.unsafe(fm.id)) - .flatMap(fmId => - ctx.store - .transact(RAttachment.updateFileIdAndName(ra.id, fmId, newName)) - .map(_ => fmId) - ) + .flatMap(fmId => updateAttachment[F](ctx, ra, fmId, newName).map(_ => fmId)) .map(fmId => ra.copy(fileId = fmId, name = newName)) } + + private def updateAttachment[F[_]: Sync]( + ctx: Context[F, _], + ra: RAttachment, + fmId: Ident, + newName: Option[String] + ): F[Unit] = + for { + oldFile <- ctx.store.transact(RAttachment.findById(ra.id)) + _ <- + ctx.store + .transact(RAttachment.updateFileIdAndName(ra.id, fmId, newName)) + _ <- oldFile match { + case Some(raPrev) => + for { + sameFile <- + ctx.store + .transact(RAttachmentSource.isSameFile(ra.id, raPrev.fileId)) + _ <- + if (sameFile) ().pure[F] + else + ctx.logger.info("Deleting previous attachment file") *> + ctx.store.bitpeace + .delete(raPrev.fileId.id) + .compile + .drain + .attempt + .flatMap { + case Right(_) => ().pure[F] + case Left(ex) => + ctx.logger + .error(ex)(s"Cannot delete previous attachment file: ${raPrev}") + + } + } yield () + case None => + ().pure[F] + } + } yield () } diff --git a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala index 139ec8f6..72eefa39 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala @@ -27,6 +27,17 @@ object ProcessItem { .flatMap(SetGivenData[F](itemOps)) .flatMap(Task.setProgress(99)) + def processAttachments[F[_]: ConcurrentEffect: ContextShift]( + cfg: Config, + fts: FtsClient[F] + )(item: ItemData): Task[F, ProcessItemArgs, ItemData] = + ConvertPdf(cfg.convert, item) + .flatMap(Task.setProgress(30)) + .flatMap(TextExtraction(cfg.extraction, fts)) + .flatMap(Task.setProgress(60)) + .flatMap(analysisOnly[F](cfg)) + .flatMap(Task.setProgress(90)) + def analysisOnly[F[_]: Sync]( cfg: Config )(item: ItemData): Task[F, ProcessItemArgs, ItemData] = diff --git a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala new file mode 100644 index 00000000..8f5e11f2 --- /dev/null +++ b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala @@ -0,0 +1,131 @@ +package docspell.joex.process + +import cats.data.OptionT +import cats.effect._ +import cats.implicits._ + +import docspell.common._ +import docspell.ftsclient.FtsClient +import docspell.joex.Config +import docspell.joex.scheduler.Context +import docspell.joex.scheduler.Task +import docspell.store.records.RAttachment +import docspell.store.records.RAttachmentSource +import docspell.store.records.RCollective +import docspell.store.records.RItem + +object ReProcessItem { + type Args = ReProcessItemArgs + + def apply[F[_]: ConcurrentEffect: ContextShift]( + cfg: Config, + fts: FtsClient[F] + ): Task[F, Args, Unit] = + loadItem[F] + .flatMap(safeProcess[F](cfg, fts)) + .map(_ => ()) + + def onCancel[F[_]: Sync: ContextShift]: Task[F, Args, Unit] = + logWarn("Now cancelling re-processing.") + + // --- Helpers + + private def contains[F[_]](ctx: Context[F, Args]): RAttachment => Boolean = { + val selection = ctx.args.attachments.toSet + if (selection.isEmpty) (_ => true) + else ra => selection.contains(ra.id) + } + + def loadItem[F[_]: Sync]: Task[F, Args, ItemData] = + Task { ctx => + (for { + item <- OptionT(ctx.store.transact(RItem.findById(ctx.args.itemId))) + attach <- OptionT.liftF(ctx.store.transact(RAttachment.findByItem(item.id))) + asrc <- + OptionT.liftF(ctx.store.transact(RAttachmentSource.findByItem(ctx.args.itemId))) + asrcMap = asrc.map(s => s.id -> s).toMap + // copy the original files over to attachments to run the default processing task + // the processing doesn't touch the original files, only RAttachments + attachSrc = + attach + .filter(contains(ctx)) + .flatMap(a => + asrcMap.get(a.id).map { src => + a.copy(fileId = src.fileId, name = src.name) + } + ) + } yield ItemData( + item, + attachSrc, + Vector.empty, + Vector.empty, + asrcMap.view.mapValues(_.fileId).toMap, + MetaProposalList.empty, + Nil + )).getOrElseF( + Sync[F].raiseError(new Exception(s"Item not found: ${ctx.args.itemId.id}")) + ) + } + + def processFiles[F[_]: ConcurrentEffect: ContextShift]( + cfg: Config, + fts: FtsClient[F], + data: ItemData + ): Task[F, Args, ItemData] = { + + val convertArgs: Language => Args => F[ProcessItemArgs] = + lang => + args => + ProcessItemArgs( + ProcessItemArgs.ProcessMeta( + data.item.cid, + args.itemId.some, + lang, + None, //direction + "", //source-id + None, //folder + Seq.empty + ), + Nil + ).pure[F] + + getLanguage[F].flatMap { lang => + ProcessItem + .processAttachments[F](cfg, fts)(data) + .contramap[Args](convertArgs(lang)) + } + } + + def getLanguage[F[_]: Sync]: Task[F, Args, Language] = + Task { ctx => + (for { + coll <- OptionT(ctx.store.transact(RCollective.findByItem(ctx.args.itemId))) + lang = coll.language + } yield lang).getOrElse(Language.German) + } + + def isLastRetry[F[_]: Sync]: Task[F, Args, Boolean] = + Task(_.isLastRetry) + + def safeProcess[F[_]: ConcurrentEffect: ContextShift]( + cfg: Config, + fts: FtsClient[F] + )(data: ItemData): Task[F, Args, ItemData] = + isLastRetry[F].flatMap { + case true => + processFiles[F](cfg, fts, data).attempt + .flatMap({ + case Right(d) => + Task.pure(d) + case Left(ex) => + logWarn[F]( + "Processing failed on last retry." + ).andThen(_ => Sync[F].raiseError(ex)) + }) + case false => + processFiles[F](cfg, fts, data) + } + + private def logWarn[F[_]](msg: => String): Task[F, Args, Unit] = + Task(_.logger.warn(msg)) +} diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 7833b28e..c8831ed4 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1796,6 +1796,31 @@ paths: application/json: schema: $ref: "#/components/schemas/ItemProposals" + /sec/item/{itemId}/reprocess: + post: + tags: [ Item ] + summary: Start reprocessing the files of the item. + description: | + This submits a job that will re-process the files (either all + or the ones specified) of the item and replace the metadata. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/StringList" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + + /sec/item/{itemId}/attachment/movebefore: post: tags: [ Item ] diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 8f51d79a..d932d407 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -279,6 +279,15 @@ object ItemRoutes { resp <- Ok(Conversions.basicResult(res, "Attachment moved.")) } yield resp + case req @ POST -> Root / Ident(id) / "reprocess" => + for { + data <- req.as[StringList] + ids = data.items.flatMap(s => Ident.fromString(s).toOption) + _ <- logger.fdebug(s"Re-process item ${id.id}") + res <- backend.upload.reprocess(id, ids, user.account, true) + resp <- Ok(Conversions.basicResult(res)) + } yield resp + case DELETE -> Root / Ident(id) => for { n <- backend.item.deleteItem(id, user.account.collective) diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala index 61d676b6..8c93de42 100644 --- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala +++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala @@ -71,6 +71,16 @@ object RAttachment { commas(fileId.setTo(fId), name.setTo(fname)) ).update.run + def updateFileId( + attachId: Ident, + fId: Ident + ): ConnectionIO[Int] = + updateRow( + table, + id.is(attachId), + fileId.setTo(fId) + ).update.run + def updatePosition(attachId: Ident, pos: Int): ConnectionIO[Int] = updateRow(table, id.is(attachId), position.setTo(pos)).update.run diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachmentSource.scala b/modules/store/src/main/scala/docspell/store/records/RAttachmentSource.scala index 58b0a6c7..d732ecff 100644 --- a/modules/store/src/main/scala/docspell/store/records/RAttachmentSource.scala +++ b/modules/store/src/main/scala/docspell/store/records/RAttachmentSource.scala @@ -42,6 +42,12 @@ object RAttachmentSource { def findById(attachId: Ident): ConnectionIO[Option[RAttachmentSource]] = selectSimple(all, table, id.is(attachId)).query[RAttachmentSource].option + def isSameFile(attachId: Ident, file: Ident): ConnectionIO[Boolean] = + selectCount(id, table, and(id.is(attachId), fileId.is(file))) + .query[Int] + .unique + .map(_ > 0) + def delete(attachId: Ident): ConnectionIO[Int] = deleteFrom(table, id.is(attachId)).update.run @@ -64,6 +70,17 @@ object RAttachmentSource { selectSimple(all.map(_.prefix("a")), from, where).query[RAttachmentSource].option } + def findByItem(itemId: Ident): ConnectionIO[Vector[RAttachmentSource]] = { + val sId = Columns.id.prefix("s") + val aId = RAttachment.Columns.id.prefix("a") + val aItem = RAttachment.Columns.itemId.prefix("a") + + val from = table ++ fr"s INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ sId.is(aId) + selectSimple(all.map(_.prefix("s")), from, aItem.is(itemId)) + .query[RAttachmentSource] + .to[Vector] + } + def findByItemWithMeta( id: Ident ): ConnectionIO[Vector[(RAttachmentSource, FileMeta)]] = { diff --git a/modules/store/src/main/scala/docspell/store/records/RCollective.scala b/modules/store/src/main/scala/docspell/store/records/RCollective.scala index 9d27bd1e..22115d5e 100644 --- a/modules/store/src/main/scala/docspell/store/records/RCollective.scala +++ b/modules/store/src/main/scala/docspell/store/records/RCollective.scala @@ -75,6 +75,14 @@ object RCollective { sql.query[RCollective].option } + def findByItem(itemId: Ident): ConnectionIO[Option[RCollective]] = { + val iColl = RItem.Columns.cid.prefix("i") + val iId = RItem.Columns.id.prefix("i") + val cId = id.prefix("c") + val from = RItem.table ++ fr"i INNER JOIN" ++ table ++ fr"c ON" ++ iColl.is(cId) + selectSimple(all.map(_.prefix("c")), from, iId.is(itemId)).query[RCollective].option + } + def existsById(cid: Ident): ConnectionIO[Boolean] = { val sql = selectCount(id, table, id.is(cid)) sql.query[Int].unique.map(_ > 0) @@ -90,5 +98,19 @@ object RCollective { sql.query[RCollective].stream } + def findByAttachment(attachId: Ident): ConnectionIO[Option[RCollective]] = { + val iColl = RItem.Columns.cid.prefix("i") + val iId = RItem.Columns.id.prefix("i") + val aItem = RAttachment.Columns.itemId.prefix("a") + val aId = RAttachment.Columns.id.prefix("a") + val cId = Columns.id.prefix("c") + + val from = table ++ fr"c INNER JOIN" ++ + RItem.table ++ fr"i ON" ++ cId.is(iColl) ++ fr"INNER JOIN" ++ + RAttachment.table ++ fr"a ON" ++ aItem.is(iId) + + selectSimple(all, from, aId.is(attachId)).query[RCollective].option + } + case class Settings(language: Language, integrationEnabled: Boolean) } diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala index e961e8b2..a0025ddb 100644 --- a/modules/store/src/main/scala/docspell/store/records/RItem.scala +++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala @@ -314,6 +314,9 @@ object RItem { def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] = selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option + def findById(itemId: Ident): ConnectionIO[Option[RItem]] = + selectSimple(all, table, id.is(itemId)).query[RItem].option + def checkByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[Ident]] = selectSimple(Seq(id), table, and(id.is(itemId), cid.is(coll))).query[Ident].option From 41ea071555722e17046c5d9e9ace8c29f36f28b5 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Wed, 12 Aug 2020 23:56:29 +0200 Subject: [PATCH 2/6] Add a task to convert all pdfs that have not been converted --- .../docspell/common/ConvertAllPdfArgs.scala | 14 ++ .../scala/docspell/joex/JoexAppImpl.scala | 16 ++ .../joex/pdfconv/ConvertAllPdfTask.scala | 68 ++++++++ .../docspell/joex/pdfconv/PdfConvTask.scala | 155 ++++++++++++++++++ .../docspell/store/records/RAttachment.scala | 29 ++++ .../docspell/store/records/RCollective.scala | 2 +- 6 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala create mode 100644 modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala create mode 100644 modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala diff --git a/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala b/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala new file mode 100644 index 00000000..d4ae5ba7 --- /dev/null +++ b/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala @@ -0,0 +1,14 @@ +package docspell.common + +import io.circe._ +import io.circe.generic.semiauto._ + +case class ConvertAllPdfArgs(collective: Option[Ident]) + +object ConvertAllPdfArgs { + val taskName = Ident.unsafe("submit-pdf-migration-tasks") + implicit val jsonDecoder: Decoder[ConvertAllPdfArgs] = + deriveDecoder[ConvertAllPdfArgs] + implicit val jsonEncoder: Encoder[ConvertAllPdfArgs] = + deriveEncoder[ConvertAllPdfArgs] +} diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala index 9882455c..f07e089e 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala @@ -13,6 +13,8 @@ import docspell.ftssolr.SolrFtsClient import docspell.joex.fts.{MigrationTask, ReIndexTask} import docspell.joex.hk._ import docspell.joex.notify._ +import docspell.joex.pdfconv.ConvertAllPdfTask +import docspell.joex.pdfconv.PdfConvTask import docspell.joex.process.ItemHandler import docspell.joex.process.ReProcessItem import docspell.joex.scanmailbox._ @@ -139,6 +141,20 @@ object JoexAppImpl { HouseKeepingTask.onCancel[F] ) ) + .withTask( + JobTask.json( + PdfConvTask.taskName, + PdfConvTask[F](cfg), + PdfConvTask.onCancel[F] + ) + ) + .withTask( + JobTask.json( + ConvertAllPdfArgs.taskName, + ConvertAllPdfTask[F](queue, joex), + ConvertAllPdfTask.onCancel[F] + ) + ) .resource psch <- PeriodicScheduler.create( cfg.periodicScheduler, diff --git a/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala b/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala new file mode 100644 index 00000000..c40d0783 --- /dev/null +++ b/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala @@ -0,0 +1,68 @@ +package docspell.joex.pdfconv + +import cats.effect._ +import cats.implicits._ +import fs2.{Chunk, Stream} + +import docspell.backend.ops.OJoex +import docspell.common._ +import docspell.joex.scheduler.{Context, Task} +import docspell.store.queue.JobQueue +import docspell.store.records.RAttachment +import docspell.store.records._ + +object ConvertAllPdfTask { + type Args = ConvertAllPdfArgs + + def apply[F[_]: Sync](queue: JobQueue[F], joex: OJoex[F]): Task[F, Args, Unit] = + Task { ctx => + for { + _ <- ctx.logger.info("Converting older pdfs using ocrmypdf") + n <- submitConversionJobs(ctx, queue) + _ <- ctx.logger.info(s"Submitted $n jobs for file conversion") + _ <- joex.notifyAllNodes + } yield () + } + + def onCancel[F[_]: Sync]: Task[F, Args, Unit] = + Task.log(_.warn("Cancelling convert-old-pdf task")) + + def submitConversionJobs[F[_]: Sync]( + ctx: Context[F, Args], + queue: JobQueue[F] + ): F[Int] = + ctx.store + .transact(RAttachment.findNonConvertedPdf(ctx.args.collective, 50)) + .chunks + .flatMap(createJobs[F](ctx)) + .chunks + .evalMap(jobs => queue.insertAll(jobs.toVector).map(_ => jobs.size)) + .evalTap(n => ctx.logger.debug(s"Submitted $n jobs …")) + .compile + .foldMonoid + + private def createJobs[F[_]: Sync]( + ctx: Context[F, Args] + )(ras: Chunk[RAttachment]): Stream[F, RJob] = { + val collectiveOrSystem = ctx.args.collective.getOrElse(DocspellSystem.taskGroup) + + def mkJob(ra: RAttachment): F[RJob] = + for { + id <- Ident.randomId[F] + now <- Timestamp.current[F] + } yield RJob.newJob( + id, + PdfConvTask.taskName, + collectiveOrSystem, + PdfConvTask.Args(ra.id), + s"Convert pdf ${ra.id.id}/${ra.name.getOrElse("-")}", + now, + collectiveOrSystem, + Priority.Low, + Some(ra.id) + ) + + val jobs = ras.traverse(mkJob) + Stream.evalUnChunk(jobs) + } +} diff --git a/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala b/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala new file mode 100644 index 00000000..07cc7c36 --- /dev/null +++ b/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala @@ -0,0 +1,155 @@ +package docspell.joex.pdfconv + +import cats.data.Kleisli +import cats.data.OptionT +import cats.effect._ +import cats.implicits._ +import fs2.Stream + +import docspell.common._ +import docspell.convert.ConversionResult +import docspell.convert.extern.OcrMyPdf +import docspell.joex.Config +import docspell.joex.scheduler.{Context, Task} +import docspell.store.records._ + +import bitpeace.FileMeta +import bitpeace.Mimetype +import bitpeace.MimetypeHint +import bitpeace.RangeDef +import io.circe.generic.semiauto._ +import io.circe.{Decoder, Encoder} + +/** Converts the given attachment file using ocrmypdf if it is a pdf + * and has not already been converted (the source file is the same as + * in the attachment). + */ +object PdfConvTask { + case class Args(attachId: Ident) + object Args { + implicit val jsonDecoder: Decoder[Args] = + deriveDecoder[Args] + implicit val jsonEncoder: Encoder[Args] = + deriveEncoder[Args] + } + + val taskName = Ident.unsafe("pdf-files-migration") + + def apply[F[_]: Sync: ContextShift](cfg: Config): Task[F, Args, Unit] = + Task { ctx => + for { + _ <- ctx.logger.info(s"Converting pdf file ${ctx.args} using ocrmypdf") + meta <- checkInputs(cfg, ctx) + _ <- meta.traverse(fm => convert(cfg, ctx, fm)) + } yield () + } + + def onCancel[F[_]: Sync]: Task[F, Args, Unit] = + Task.log(_.warn("Cancelling pdfconv task")) + + // --- Helper + + // check if file exists and if it is pdf and if source id is the same and if ocrmypdf is enabled + def checkInputs[F[_]: Sync](cfg: Config, ctx: Context[F, Args]): F[Option[FileMeta]] = { + val none: Option[FileMeta] = None + val checkSameFiles = + (for { + ra <- OptionT(ctx.store.transact(RAttachment.findById(ctx.args.attachId))) + isSame <- OptionT.liftF( + ctx.store.transact(RAttachmentSource.isSameFile(ra.id, ra.fileId)) + ) + } yield isSame).getOrElse(false) + val existsPdf = + for { + meta <- ctx.store.transact(RAttachment.findMeta(ctx.args.attachId)) + res = meta.filter(_.mimetype.matches(Mimetype.`application/pdf`)) + _ <- + if (res.isEmpty) + ctx.logger.info( + s"The attachment ${ctx.args.attachId} doesn't exist or is no pdf: $meta" + ) + else ().pure[F] + } yield res + + if (cfg.convert.ocrmypdf.enabled) + checkSameFiles.flatMap { + case true => existsPdf + case false => + ctx.logger.info( + s"The attachment ${ctx.args.attachId} already has been converted. Skipping." + ) *> + none.pure[F] + } + else none.pure[F] + } + + def convert[F[_]: Sync: ContextShift]( + cfg: Config, + ctx: Context[F, Args], + in: FileMeta + ): F[Unit] = { + val bp = ctx.store.bitpeace + val data = Stream + .emit(in) + .through(bp.fetchData2(RangeDef.all)) + + val storeResult: ConversionResult.Handler[F, Unit] = + Kleisli({ + case ConversionResult.SuccessPdf(file) => + storeToAttachment(ctx, in, file) + + case ConversionResult.SuccessPdfTxt(file, _) => + storeToAttachment(ctx, in, file) + + case ConversionResult.UnsupportedFormat(mime) => + ctx.logger.warn( + s"Unable to convert '${mime}' file ${ctx.args}: unsupported format." + ) + + case ConversionResult.InputMalformed(mime, reason) => + ctx.logger.warn(s"Unable to convert '${mime}' file ${ctx.args}: $reason") + + case ConversionResult.Failure(ex) => + ctx.logger.error(ex)(s"Failure converting file ${ctx.args}: ${ex.getMessage}") + }) + + def ocrMyPdf(lang: Language): F[Unit] = + OcrMyPdf.toPDF[F, Unit]( + cfg.convert.ocrmypdf, + lang, + in.chunksize, + ctx.blocker, + ctx.logger + )(data, storeResult) + + for { + lang <- getLanguage(ctx) + _ <- ocrMyPdf(lang) + } yield () + } + + def getLanguage[F[_]: Sync](ctx: Context[F, Args]): F[Language] = + (for { + coll <- OptionT(ctx.store.transact(RCollective.findByAttachment(ctx.args.attachId))) + lang = coll.language + } yield lang).getOrElse(Language.German) + + def storeToAttachment[F[_]: Sync]( + ctx: Context[F, Args], + meta: FileMeta, + newFile: Stream[F, Byte] + ): F[Unit] = { + val mimeHint = MimetypeHint.advertised(meta.mimetype.asString) + for { + time <- Timestamp.current[F] + fid <- Ident.randomId[F] + _ <- + ctx.store.bitpeace + .saveNew(newFile, meta.chunksize, mimeHint, Some(fid.id), time.value) + .compile + .lastOrError + _ <- ctx.store.transact(RAttachment.updateFileId(ctx.args.attachId, fid)) + } yield () + } + +} diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala index 8c93de42..fa6bb724 100644 --- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala +++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala @@ -1,6 +1,7 @@ package docspell.store.records import cats.implicits._ +import fs2.Stream import docspell.common._ import docspell.store.impl.Implicits._ @@ -197,4 +198,32 @@ object RAttachment { def findItemId(attachId: Ident): ConnectionIO[Option[Ident]] = selectSimple(Seq(itemId), table, id.is(attachId)).query[Ident].option + + def findNonConvertedPdf( + coll: Option[Ident], + chunkSize: Int + ): Stream[ConnectionIO, RAttachment] = { + val aId = Columns.id.prefix("a") + val aItem = Columns.itemId.prefix("a") + val aFile = Columns.fileId.prefix("a") + val sId = RAttachmentSource.Columns.id.prefix("s") + val sFile = RAttachmentSource.Columns.fileId.prefix("s") + val iId = RItem.Columns.id.prefix("i") + val iColl = RItem.Columns.cid.prefix("i") + val mId = RFileMeta.Columns.id.prefix("m") + val mType = RFileMeta.Columns.mimetype.prefix("m") + val pdfType = "application/pdf%" + + val from = table ++ fr"a INNER JOIN" ++ + RAttachmentSource.table ++ fr"s ON" ++ sId.is(aId) ++ fr"INNER JOIN" ++ + RItem.table ++ fr"i ON" ++ iId.is(aItem) ++ fr"INNER JOIN" ++ + RFileMeta.table ++ fr"m ON" ++ aFile.is(mId) + val where = coll match { + case Some(cid) => and(iColl.is(cid), aFile.is(sFile), mType.lowerLike(pdfType)) + case None => and(aFile.is(sFile), mType.lowerLike(pdfType)) + } + selectSimple(all.map(_.prefix("a")), from, where) + .query[RAttachment] + .streamWithChunkSize(chunkSize) + } } diff --git a/modules/store/src/main/scala/docspell/store/records/RCollective.scala b/modules/store/src/main/scala/docspell/store/records/RCollective.scala index 22115d5e..fa40e374 100644 --- a/modules/store/src/main/scala/docspell/store/records/RCollective.scala +++ b/modules/store/src/main/scala/docspell/store/records/RCollective.scala @@ -109,7 +109,7 @@ object RCollective { RItem.table ++ fr"i ON" ++ cId.is(iColl) ++ fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ aItem.is(iId) - selectSimple(all, from, aId.is(attachId)).query[RCollective].option + selectSimple(all.map(_.prefix("c")), from, aId.is(attachId)).query[RCollective].option } case class Settings(language: Language, integrationEnabled: Boolean) From 69674eb485c00bced53e334570287bf17eac2499 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Thu, 13 Aug 2020 01:01:02 +0200 Subject: [PATCH 3/6] Improve job-queue query to make sure jobs across all states show up --- .../scala/docspell/backend/ops/OJob.scala | 4 ++- .../restserver/routes/JobQueueRoutes.scala | 2 +- .../scala/docspell/store/queries/QJob.scala | 33 ++++++++++++++----- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala b/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala index 9a05337c..ade2fda0 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OJob.scala @@ -48,7 +48,9 @@ object OJob { def queueState(collective: Ident, maxResults: Int): F[CollectiveQueueState] = store - .transact(QJob.queueStateSnapshot(collective).take(maxResults.toLong)) + .transact( + QJob.queueStateSnapshot(collective, maxResults.toLong) + ) .map(t => JobDetail(t._1, t._2)) .compile .toVector diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala index fc605f74..4a34b219 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala @@ -21,7 +21,7 @@ object JobQueueRoutes { HttpRoutes.of { case GET -> Root / "state" => for { - js <- backend.job.queueState(user.account.collective, 200) + js <- backend.job.queueState(user.account.collective, 40) res = Conversions.mkJobQueueState(js) resp <- Ok(res) } yield resp diff --git a/modules/store/src/main/scala/docspell/store/queries/QJob.scala b/modules/store/src/main/scala/docspell/store/queries/QJob.scala index 99f94b67..f3521ed9 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QJob.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QJob.scala @@ -209,7 +209,8 @@ object QJob { store.transact(RJob.findFromIds(ids)) def queueStateSnapshot( - collective: Ident + collective: Ident, + max: Long ): Stream[ConnectionIO, (RJob, Vector[RJobLog])] = { val JC = RJob.Columns val waiting: Set[JobState] = Set(JobState.Waiting, JobState.Stuck, JobState.Scheduled) @@ -218,18 +219,34 @@ object QJob { def selectJobs(now: Timestamp): Stream[ConnectionIO, RJob] = { val refDate = now.minusHours(24) - val sql = selectSimple( + + val runningJobs = (selectSimple( + JC.all, + RJob.table, + and(JC.group.is(collective), JC.state.isOneOf(running.toSeq)) + ) ++ orderBy(JC.submitted.desc)).query[RJob].stream + + val waitingJobs = (selectSimple( JC.all, RJob.table, and( JC.group.is(collective), - or( - and(JC.state.isOneOf(done.toSeq), JC.submitted.isGt(refDate)), - JC.state.isOneOf((running ++ waiting).toSeq) - ) + JC.state.isOneOf(waiting.toSeq), + JC.submitted.isGt(refDate) ) - ) - (sql ++ orderBy(JC.submitted.desc)).query[RJob].stream + ) ++ orderBy(JC.submitted.desc)).query[RJob].stream.take(max) + + val doneJobs = (selectSimple( + JC.all, + RJob.table, + and( + JC.group.is(collective), + JC.state.isOneOf(done.toSeq), + JC.submitted.isGt(refDate) + ) + ) ++ orderBy(JC.submitted.desc)).query[RJob].stream.take(max) + + runningJobs ++ waitingJobs ++ doneJobs } def selectLogs(job: RJob): ConnectionIO[Vector[RJobLog]] = From 081c4da903f8c78e4e13f9df900bcae220a97a3f Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Thu, 13 Aug 2020 01:03:42 +0200 Subject: [PATCH 4/6] Add a route to trigger the convert-all-pdf task for a collective --- .../scala/docspell/backend/JobFactory.scala | 21 +++++++++++++++++++ .../scala/docspell/backend/ops/OUpload.scala | 15 +++++++++++++ .../restserver/routes/ItemRoutes.scala | 7 +++++++ 3 files changed, 43 insertions(+) diff --git a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala index 96399ffa..396352b4 100644 --- a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala +++ b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala @@ -8,6 +8,27 @@ import docspell.store.records.RJob object JobFactory { + def convertAllPdfs[F[_]: Sync]( + collective: Option[Ident], + account: AccountId, + prio: Priority + ): F[RJob] = + for { + id <- Ident.randomId[F] + now <- Timestamp.current[F] + job = RJob.newJob( + id, + ConvertAllPdfArgs.taskName, + account.collective, + ConvertAllPdfArgs(collective), + s"Convert all pdfs not yet converted", + now, + account.user, + prio, + None + ) + } yield job + def reprocessItem[F[_]: Sync]( args: ReProcessItemArgs, account: AccountId, diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala index a6fbce49..c6edbfb2 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala @@ -57,6 +57,11 @@ trait OUpload[F[_]] { notifyJoex: Boolean ): F[OUpload.UploadResult] + def convertAllPdf( + collective: Option[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[OUpload.UploadResult] } object OUpload { @@ -187,6 +192,16 @@ object OUpload { res <- OptionT.liftF(submitJobs(notifyJoex)(Vector(job))) } yield res).getOrElse(UploadResult.noItem) + def convertAllPdf( + collective: Option[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[OUpload.UploadResult] = + for { + job <- JobFactory.convertAllPdfs(collective, account, Priority.Low) + res <- submitJobs(notifyJoex)(Vector(job)) + } yield res + private def submitJobs( notifyJoex: Boolean )(jobs: Vector[RJob]): F[OUpload.UploadResult] = diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index d932d407..49363696 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -31,6 +31,13 @@ object ItemRoutes { import dsl._ HttpRoutes.of { + case POST -> Root / "convertallpdfs" => + for { + res <- + backend.upload.convertAllPdf(user.account.collective.some, user.account, true) + resp <- Ok(Conversions.basicResult(res)) + } yield resp + case req @ POST -> Root / "search" => for { mask <- req.as[ItemSearch] From 3986487f11ecdcef03973e1bcd204cb9800ed404 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Thu, 13 Aug 2020 20:52:43 +0200 Subject: [PATCH 5/6] Add api docs and cleanup --- .../scala/docspell/backend/BackendApp.scala | 2 +- .../scala/docspell/backend/JobFactory.scala | 9 +-- .../scala/docspell/backend/ops/OItem.scala | 60 ++++++++++++++++++- .../scala/docspell/backend/ops/OUpload.scala | 43 ------------- .../docspell/common/ConvertAllPdfArgs.scala | 12 ++++ .../scala/docspell/joex/JoexAppImpl.scala | 2 +- .../joex/pdfconv/ConvertAllPdfTask.scala | 12 ++-- .../docspell/joex/process/ProcessItem.scala | 26 ++++---- .../src/main/resources/docspell-openapi.yml | 40 ++++++++++++- .../restserver/routes/ItemRoutes.scala | 12 ++-- .../scala/docspell/store/queue/JobQueue.scala | 10 ++++ 11 files changed, 155 insertions(+), 73 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index 72ce0138..6ff3c73e 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -61,7 +61,7 @@ object BackendApp { uploadImpl <- OUpload(store, queue, cfg.files, joexImpl) nodeImpl <- ONode(store) jobImpl <- OJob(store, joexImpl) - itemImpl <- OItem(store, ftsClient) + itemImpl <- OItem(store, ftsClient, queue, joexImpl) itemSearchImpl <- OItemSearch(store) fulltextImpl <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl) javaEmil = diff --git a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala index 396352b4..bc05a188 100644 --- a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala +++ b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala @@ -25,15 +25,16 @@ object JobFactory { now, account.user, prio, - None + collective + .map(c => c / ConvertAllPdfArgs.taskName) + .orElse(ConvertAllPdfArgs.taskName.some) ) } yield job def reprocessItem[F[_]: Sync]( args: ReProcessItemArgs, account: AccountId, - prio: Priority, - tracker: Option[Ident] + prio: Priority ): F[RJob] = for { id <- Ident.randomId[F] @@ -47,7 +48,7 @@ object JobFactory { now, account.user, prio, - tracker + Some(ReProcessItemArgs.taskName / args.itemId) ) } yield job diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala index 4919fdfe..da3efce2 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -4,10 +4,12 @@ import cats.data.OptionT import cats.effect.{Effect, Resource} import cats.implicits._ +import docspell.backend.JobFactory import docspell.common._ import docspell.ftsclient.FtsClient import docspell.store.UpdateResult import docspell.store.queries.{QAttachment, QItem} +import docspell.store.queue.JobQueue import docspell.store.records._ import docspell.store.{AddResult, Store} @@ -76,11 +78,38 @@ trait OItem[F[_]] { name: Option[String], collective: Ident ): F[AddResult] + + /** Submits the item for re-processing. The list of attachment ids can + * be used to only re-process a subset of the item's attachments. + * If this list is empty, all attachments are reprocessed. This + * call only submits the job into the queue. + */ + def reprocess( + item: Ident, + attachments: List[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[UpdateResult] + + /** Submits a task that finds all non-converted pdfs and triggers + * converting them using ocrmypdf. Each file is converted by a + * separate task. + */ + def convertAllPdf( + collective: Option[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[UpdateResult] } object OItem { - def apply[F[_]: Effect](store: Store[F], fts: FtsClient[F]): Resource[F, OItem[F]] = + def apply[F[_]: Effect]( + store: Store[F], + fts: FtsClient[F], + queue: JobQueue[F], + joex: OJoex[F] + ): Resource[F, OItem[F]] = for { otag <- OTag(store) oorg <- OOrganization(store) @@ -400,6 +429,35 @@ object OItem { ) ) + def reprocess( + item: Ident, + attachments: List[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[UpdateResult] = + (for { + _ <- OptionT( + store.transact(RItem.findByIdAndCollective(item, account.collective)) + ) + args = ReProcessItemArgs(item, attachments) + job <- OptionT.liftF( + JobFactory.reprocessItem[F](args, account, Priority.Low) + ) + _ <- OptionT.liftF(queue.insertIfNew(job)) + _ <- OptionT.liftF(if (notifyJoex) joex.notifyAllNodes else ().pure[F]) + } yield UpdateResult.success).getOrElse(UpdateResult.notFound) + + def convertAllPdf( + collective: Option[Ident], + account: AccountId, + notifyJoex: Boolean + ): F[UpdateResult] = + for { + job <- JobFactory.convertAllPdfs[F](collective, account, Priority.Low) + _ <- queue.insertIfNew(job) + _ <- if (notifyJoex) joex.notifyAllNodes else ().pure[F] + } yield UpdateResult.success + private def onSuccessIgnoreError(update: F[Unit])(ar: AddResult): F[Unit] = ar match { case AddResult.Success => diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala index c6edbfb2..a9145f72 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUpload.scala @@ -44,24 +44,6 @@ trait OUpload[F[_]] { case Left(srcId) => submit(data, srcId, notifyJoex, itemId) } - - /** Submits the item for re-processing. The list of attachment ids can - * be used to only re-process a subset of the item's attachments. - * If this list is empty, all attachments are reprocessed. This - * call only submits the job into the queue. - */ - def reprocess( - item: Ident, - attachments: List[Ident], - account: AccountId, - notifyJoex: Boolean - ): F[OUpload.UploadResult] - - def convertAllPdf( - collective: Option[Ident], - account: AccountId, - notifyJoex: Boolean - ): F[OUpload.UploadResult] } object OUpload { @@ -177,31 +159,6 @@ object OUpload { result <- OptionT.liftF(submit(updata, accId, notifyJoex, itemId)) } yield result).getOrElse(UploadResult.noSource) - def reprocess( - item: Ident, - attachments: List[Ident], - account: AccountId, - notifyJoex: Boolean - ): F[UploadResult] = - (for { - _ <- - OptionT(store.transact(RItem.findByIdAndCollective(item, account.collective))) - args = ReProcessItemArgs(item, attachments) - job <- - OptionT.liftF(JobFactory.reprocessItem[F](args, account, Priority.Low, None)) - res <- OptionT.liftF(submitJobs(notifyJoex)(Vector(job))) - } yield res).getOrElse(UploadResult.noItem) - - def convertAllPdf( - collective: Option[Ident], - account: AccountId, - notifyJoex: Boolean - ): F[OUpload.UploadResult] = - for { - job <- JobFactory.convertAllPdfs(collective, account, Priority.Low) - res <- submitJobs(notifyJoex)(Vector(job)) - } yield res - private def submitJobs( notifyJoex: Boolean )(jobs: Vector[RJob]): F[OUpload.UploadResult] = diff --git a/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala b/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala index d4ae5ba7..eb2978d7 100644 --- a/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala +++ b/modules/common/src/main/scala/docspell/common/ConvertAllPdfArgs.scala @@ -3,12 +3,24 @@ package docspell.common import io.circe._ import io.circe.generic.semiauto._ +/** Arguments for the task that finds all pdf files that have not been + * converted and submits for each a job that will convert the file + * using ocrmypdf. + * + * If the `collective` argument is present, then this task and the + * ones that are submitted by this task run in the realm of the + * collective (and only their files are considered). If it is empty, + * it is a system task and all files are considered. + */ case class ConvertAllPdfArgs(collective: Option[Ident]) object ConvertAllPdfArgs { + val taskName = Ident.unsafe("submit-pdf-migration-tasks") + implicit val jsonDecoder: Decoder[ConvertAllPdfArgs] = deriveDecoder[ConvertAllPdfArgs] + implicit val jsonEncoder: Encoder[ConvertAllPdfArgs] = deriveEncoder[ConvertAllPdfArgs] } diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala index f07e089e..bc415446 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala @@ -87,7 +87,7 @@ object JoexAppImpl { joex <- OJoex(client, store) upload <- OUpload(store, queue, cfg.files, joex) fts <- createFtsClient(cfg)(httpClient) - itemOps <- OItem(store, fts) + itemOps <- OItem(store, fts, queue, joex) javaEmil = JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug)) sch <- SchedulerBuilder(cfg.scheduler, blocker, store) diff --git a/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala b/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala index c40d0783..019894fa 100644 --- a/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/pdfconv/ConvertAllPdfTask.scala @@ -11,15 +11,19 @@ import docspell.store.queue.JobQueue import docspell.store.records.RAttachment import docspell.store.records._ +/* A task to find all non-converted pdf files (of a collective, or + * all) and converting them using ocrmypdf by submitting a job for + * each found file. + */ object ConvertAllPdfTask { type Args = ConvertAllPdfArgs def apply[F[_]: Sync](queue: JobQueue[F], joex: OJoex[F]): Task[F, Args, Unit] = Task { ctx => for { - _ <- ctx.logger.info("Converting older pdfs using ocrmypdf") + _ <- ctx.logger.info("Converting pdfs using ocrmypdf") n <- submitConversionJobs(ctx, queue) - _ <- ctx.logger.info(s"Submitted $n jobs for file conversion") + _ <- ctx.logger.info(s"Submitted $n file conversion jobs") _ <- joex.notifyAllNodes } yield () } @@ -36,7 +40,7 @@ object ConvertAllPdfTask { .chunks .flatMap(createJobs[F](ctx)) .chunks - .evalMap(jobs => queue.insertAll(jobs.toVector).map(_ => jobs.size)) + .evalMap(jobs => queue.insertAllIfNew(jobs.toVector).map(_ => jobs.size)) .evalTap(n => ctx.logger.debug(s"Submitted $n jobs …")) .compile .foldMonoid @@ -59,7 +63,7 @@ object ConvertAllPdfTask { now, collectiveOrSystem, Priority.Low, - Some(ra.id) + Some(PdfConvTask.taskName / ra.id) ) val jobs = ras.traverse(mkJob) diff --git a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala index 72eefa39..9b4d050f 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala @@ -17,12 +17,7 @@ object ProcessItem { )(item: ItemData): Task[F, ProcessItemArgs, ItemData] = ExtractArchive(item) .flatMap(Task.setProgress(20)) - .flatMap(ConvertPdf(cfg.convert, _)) - .flatMap(Task.setProgress(40)) - .flatMap(TextExtraction(cfg.extraction, fts)) - .flatMap(Task.setProgress(60)) - .flatMap(analysisOnly[F](cfg)) - .flatMap(Task.setProgress(80)) + .flatMap(processAttachments0(cfg, fts, (40, 60, 80))) .flatMap(LinkProposal[F]) .flatMap(SetGivenData[F](itemOps)) .flatMap(Task.setProgress(99)) @@ -31,12 +26,7 @@ object ProcessItem { cfg: Config, fts: FtsClient[F] )(item: ItemData): Task[F, ProcessItemArgs, ItemData] = - ConvertPdf(cfg.convert, item) - .flatMap(Task.setProgress(30)) - .flatMap(TextExtraction(cfg.extraction, fts)) - .flatMap(Task.setProgress(60)) - .flatMap(analysisOnly[F](cfg)) - .flatMap(Task.setProgress(90)) + processAttachments0[F](cfg, fts, (30, 60, 90))(item) def analysisOnly[F[_]: Sync]( cfg: Config @@ -45,4 +35,16 @@ object ProcessItem { .flatMap(FindProposal[F](cfg.processing)) .flatMap(EvalProposals[F]) .flatMap(SaveProposals[F]) + + private def processAttachments0[F[_]: ConcurrentEffect: ContextShift]( + cfg: Config, + fts: FtsClient[F], + progress: (Int, Int, Int) + )(item: ItemData): Task[F, ProcessItemArgs, ItemData] = + ConvertPdf(cfg.convert, item) + .flatMap(Task.setProgress(progress._1)) + .flatMap(TextExtraction(cfg.extraction, fts)) + .flatMap(Task.setProgress(progress._2)) + .flatMap(analysisOnly[F](cfg)) + .flatMap(Task.setProgress(progress._3)) } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index c8831ed4..94f84dd0 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1213,6 +1213,33 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /sec/item/convertallpdfs: + post: + tags: [ Item ] + summary: Convert all non-converted pdfs. + description: | + Submits a job that will find all pdf files that have not been + converted and converts them using the ocrmypdf tool (if + enabled). This tool has been added in version 0.9.0 and so + older files can be "migrated" this way, or maybe after + enabling the tool. + + The task finds all files of the current collective and submits + task for each file to convert. These tasks are submitted with + a low priority so that normal processing can still proceed. + + The body of the request should be empty. + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + + /sec/item/search: post: tags: [ Item ] @@ -1811,7 +1838,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/StringList" + $ref: "#/components/schemas/IdList" responses: 200: description: Ok @@ -2629,6 +2656,17 @@ paths: components: schemas: + IdList: + description: + A list of identifiers. + required: + - ids + properties: + ids: + type: array + items: + type: string + format: ident StringList: description: | A simple list of strings. diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 49363696..a033791d 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -34,8 +34,8 @@ object ItemRoutes { case POST -> Root / "convertallpdfs" => for { res <- - backend.upload.convertAllPdf(user.account.collective.some, user.account, true) - resp <- Ok(Conversions.basicResult(res)) + backend.item.convertAllPdf(user.account.collective.some, user.account, true) + resp <- Ok(Conversions.basicResult(res, "Task submitted")) } yield resp case req @ POST -> Root / "search" => @@ -288,11 +288,11 @@ object ItemRoutes { case req @ POST -> Root / Ident(id) / "reprocess" => for { - data <- req.as[StringList] - ids = data.items.flatMap(s => Ident.fromString(s).toOption) + data <- req.as[IdList] + ids = data.ids.flatMap(s => Ident.fromString(s).toOption) _ <- logger.fdebug(s"Re-process item ${id.id}") - res <- backend.upload.reprocess(id, ids, user.account, true) - resp <- Ok(Conversions.basicResult(res)) + res <- backend.item.reprocess(id, ids, user.account, true) + resp <- Ok(Conversions.basicResult(res, "Re-process task submitted.")) } yield resp case DELETE -> Root / Ident(id) => diff --git a/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala b/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala index f7d15ed5..127a45e1 100644 --- a/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala +++ b/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala @@ -28,6 +28,8 @@ trait JobQueue[F[_]] { def insertAll(jobs: Seq[RJob]): F[Unit] + def insertAllIfNew(jobs: Seq[RJob]): F[Unit] + def nextJob( prio: Ident => F[Priority], worker: Ident, @@ -81,5 +83,13 @@ object JobQueue { logger.error(ex)("Could not insert job. Skipping it.") }) + def insertAllIfNew(jobs: Seq[RJob]): F[Unit] = + jobs.toList + .traverse(j => insertIfNew(j).attempt) + .map(_.foreach { + case Right(()) => + case Left(ex) => + logger.error(ex)("Could not insert job. Skipping it.") + }) }) } From f1288d384e7fa4c48f0c897af8dc269c3da6e26c Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Thu, 13 Aug 2020 22:08:15 +0200 Subject: [PATCH 6/6] Update documentation --- tools/convert-all-pdfs.sh | 29 +++++++++++ website/site/content/docs/joex/_index.md | 2 +- .../content/docs/tools/convert-all-pdf.md | 46 ++++++++++++++++++ .../site/content/docs/webapp/sources-edit.png | Bin 0 -> 47084 bytes website/site/content/docs/webapp/uploading.md | 19 +++++--- 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100755 tools/convert-all-pdfs.sh create mode 100644 website/site/content/docs/tools/convert-all-pdf.md create mode 100644 website/site/content/docs/webapp/sources-edit.png diff --git a/tools/convert-all-pdfs.sh b/tools/convert-all-pdfs.sh new file mode 100755 index 00000000..5e47e2e1 --- /dev/null +++ b/tools/convert-all-pdfs.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Simple script to authenticate with docspell and trigger the "convert +# all pdf" route that submits a task to convert all pdf files using +# ocrmypdf. + +set -e + +BASE_URL="${1:-http://localhost:7880}" +LOGIN_URL="$BASE_URL/api/v1/open/auth/login" +TRIGGER_URL="$BASE_URL/api/v1/sec/item/convertallpdfs" + +echo "Login to trigger converting all pdfs." +echo "Using url: $BASE_URL" +echo -n "Account: " +read USER +echo -n "Password: " +read -s PASS +echo + +auth=$(curl --fail -XPOST --silent --data-binary "{\"account\":\"$USER\", \"password\":\"$PASS\"}" "$LOGIN_URL") + +if [ "$(echo $auth | jq .success)" == "true" ]; then + echo "Login successful" + auth_token=$(echo $auth | jq -r .token) + curl --fail -XPOST -H "X-Docspell-Auth: $auth_token" "$TRIGGER_URL" +else + echo "Login failed." +fi diff --git a/website/site/content/docs/joex/_index.md b/website/site/content/docs/joex/_index.md index c9ac7fd7..bc0bd517 100644 --- a/website/site/content/docs/joex/_index.md +++ b/website/site/content/docs/joex/_index.md @@ -67,7 +67,7 @@ logged in. The relevant part of the config file regarding the scheduler is shown below with some explanations. -``` +``` conf docspell.joex { # other settings left out for brevity diff --git a/website/site/content/docs/tools/convert-all-pdf.md b/website/site/content/docs/tools/convert-all-pdf.md new file mode 100644 index 00000000..a0b91aea --- /dev/null +++ b/website/site/content/docs/tools/convert-all-pdf.md @@ -0,0 +1,46 @@ ++++ +title = "Convert All PDFs" +description = "Convert all PDF files using OcrMyPdf." +weight = 60 ++++ + +# convert-all-pdf.sh + +With version 0.9.0 there was support added for another external tool, +[OCRMyPdf](https://github.com/jbarlow83/OCRmyPDF), that can convert +PDF files such that they contain the OCR-ed text layer. This tool is +optional and can be disabled. + +In order to convert all previously processed files with this tool, +there is an +[endpoint](/openapi/docspell-openapi.html#api-Item-secItemConvertallpdfsPost) +that submits a task to convert all PDF files not already converted for +your collective. + +There is no UI part to trigger this route, so you need to use curl or +the script `convert-all-pdfs.sh` in the `tools/` directory. + + +# Usage + +``` +./convert-all-pdfs.sh [docspell-base-url] +``` + +For example, if docspell is at `http://localhost:7880`: + +``` +./convert-all-pdfs.sh http://localhost:7880 +``` + +The script asks for your account name and password. It then logs in +and triggers the said endpoint. After this you should see a few tasks +running. + +There will be one task per file to convert. All these tasks are +submitted with a low priority. So files uploaded through the webapp or +a [source](@/docs/webapp/uploading.md#anonymous-upload) with a high +priority, will be preferred as [configured in the job +executor](@/docs/joex/_index.md#scheduler-config). This is to not +disturb normal processing when many conversion tasks are being +executed. diff --git a/website/site/content/docs/webapp/sources-edit.png b/website/site/content/docs/webapp/sources-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..23804991a36c3ee113473244324c6eb0458fd444 GIT binary patch literal 47084 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 DREHPh literal 0 HcmV?d00001 diff --git a/website/site/content/docs/webapp/uploading.md b/website/site/content/docs/webapp/uploading.md index 4f729435..a8f37381 100644 --- a/website/site/content/docs/webapp/uploading.md +++ b/website/site/content/docs/webapp/uploading.md @@ -29,6 +29,8 @@ scripts. For this the next variant exists. It is also possible to upload files without authentication. This should make tools that interact with docspell much easier to write. +The [Android Client App](@/docs/tools/android.md) uses these urls to +upload files. Go to "Collective Settings" and then to the "Source" tab. A *Source* identifies an endpoint where files can be uploaded anonymously. @@ -41,7 +43,7 @@ username is not visible. Example screenshot: -{{ figure(file="sources-form.png") }} +{{ figure(file="sources-edit.png") }} This example shows a source with name "test". Besides a description and a name that is only used for displaying purposes, a priority and a @@ -58,25 +60,26 @@ The source endpoint defines two urls: - `/app/upload/<id>` - `/api/v1/open/upload/item/<id>` +{{ figure(file="sources-form.png") }} + The first points to a web page where everyone could upload files into your account. You could give this url to people for sending files directly into your docspell. The second url is the API url, which accepts the requests to upload -files (it is used by the upload page, the first url). +files. This second url can be used with the [Android Client +App](@/docs/tools/android.md) to upload files. -For example, the api url can be used to upload files with curl: +Another example is to use curl for uploading files from the command +line:: ``` bash $ curl -XPOST -F file=@test.pdf http://192.168.1.95:7880/api/v1/open/upload/item/3H7hvJcDJuk-NrAW4zxsdfj-K6TMPyb6BGP-xKptVxUdqWa {"success":true,"message":"Files submitted."} ``` -You could add more `-F file=@/path/to/your/file.pdf` to upload -multiple files (note, the `@` is required by curl, so it knows that -the following is a file). There is a [script -provided](@/docs/tools/ds.md) that uses this to upload files from the -command line. +There is a [script provided](@/docs/tools/ds.md) that uses curl to +upload files from the command line more conveniently. When files are uploaded to an source endpoint, the items resulting from this uploads are marked with the name of the source. So you know