From 2a39b2f6a6a1ea449496b0cbaf00d0b36f28fb93 Mon Sep 17 00:00:00 2001
From: Rehan Mahmood <git@rehan.appaddy.uk>
Date: Tue, 31 Oct 2023 14:24:00 -0400
Subject: [PATCH] Updated following dependencies as they need changes to the
 code to work properly: - Scala - fs2 - http4s

---
 build.sbt                                     |  2 +-
 .../scala/docspell/addons/AddonArchive.scala  |  8 ++++----
 .../scala/docspell/addons/AddonExecutor.scala |  4 ++--
 .../scala/docspell/addons/AddonMeta.scala     |  4 ++--
 .../scala/docspell/addons/AddonRunner.scala   | 13 ++++++++-----
 .../addons/runner/NixFlakeRunner.scala        |  4 ++--
 .../docspell/addons/runner/RunnerUtil.scala   |  2 +-
 .../addons/runner/TrivialRunner.scala         | 12 +++++++-----
 .../docspell/analysis/TextAnalyser.scala      |  5 +++--
 .../classifier/StanfordTextClassifier.scala   |  2 +-
 .../docspell/analysis/nlp/PipelineCache.scala |  5 +++--
 .../scala/docspell/backend/BackendApp.scala   |  3 ++-
 .../docspell/backend/joex/AddonOps.scala      |  8 ++++++--
 .../docspell/backend/ops/AddonValidate.scala  |  4 ++--
 .../scala/docspell/backend/ops/OAddons.scala  |  3 ++-
 .../main/scala/docspell/common/Binary.scala   |  6 +++---
 .../scala/docspell/common/util/File.scala     |  2 +-
 .../main/scala/docspell/common/util/Zip.scala |  6 +++---
 .../scala/docspell/common/util/ZipImpl.scala  |  2 +-
 .../scala/docspell/config/ConfigFactory.scala |  9 ++++++---
 .../scala/docspell/convert/Conversion.scala   |  3 ++-
 .../docspell/convert/extern/ExternConv.scala  | 10 +++++-----
 .../docspell/convert/extern/OcrMyPdf.scala    |  4 ++--
 .../docspell/convert/extern/Tesseract.scala   |  4 ++--
 .../docspell/convert/extern/Unoconv.scala     |  4 ++--
 .../docspell/convert/extern/Weasyprint.scala  |  6 +++---
 .../docspell/convert/extern/WkHtmlPdf.scala   | 13 ++++++++++---
 .../scala/docspell/extract/Extraction.scala   |  3 ++-
 .../scala/docspell/extract/PdfExtract.scala   |  3 ++-
 .../main/scala/docspell/extract/ocr/Ocr.scala | 10 +++++-----
 .../docspell/extract/ocr/TextExtract.scala    |  5 +++--
 .../scala/docspell/files/FileSupport.scala    |  4 ++--
 .../main/scala/docspell/joex/ConfigFile.scala |  3 ++-
 .../scala/docspell/joex/JoexAppImpl.scala     | 19 +++++++++++++------
 .../main/scala/docspell/joex/JoexServer.scala |  7 ++++++-
 .../main/scala/docspell/joex/JoexTasks.scala  |  6 ++++--
 .../joex/addon/GenericItemAddonTask.scala     |  6 +++---
 .../docspell/joex/addon/ItemAddonTask.scala   |  6 +++++-
 .../docspell/joex/analysis/NerFile.scala      |  4 ++--
 .../docspell/joex/analysis/RegexNerFile.scala |  6 +++---
 .../joex/download/DownloadZipTask.scala       |  3 ++-
 .../docspell/joex/hk/CheckNodesTask.scala     |  3 ++-
 .../docspell/joex/hk/HouseKeepingTask.scala   |  3 ++-
 .../scala/docspell/joex/learn/Classify.scala  |  2 +-
 .../joex/learn/LearnClassifierTask.scala      |  7 ++++---
 .../joex/learn/LearnItemEntities.scala        | 13 +++++++------
 .../scala/docspell/joex/learn/LearnTags.scala |  8 ++++++--
 .../joex/learn/StoreClassifierModel.scala     |  2 +-
 .../multiupload/MultiUploadArchiveTask.scala  |  8 ++++++--
 .../docspell/joex/pdfconv/PdfConvTask.scala   |  7 ++++---
 .../docspell/joex/process/ConvertPdf.scala    |  5 +++--
 .../joex/process/ExtractArchive.scala         | 11 ++++++-----
 .../docspell/joex/process/ItemHandler.scala   |  5 +++--
 .../docspell/joex/process/ProcessItem.scala   |  9 +++++----
 .../docspell/joex/process/ReProcessItem.scala |  7 ++++---
 .../docspell/joex/process/RunAddons.scala     |  3 ++-
 .../docspell/joex/process/TextAnalysis.scala  |  9 +++++----
 .../joex/process/TextExtraction.scala         |  9 +++++----
 .../docspell/joexapi/client/JoexClient.scala  |  3 ++-
 .../docspell/restserver/ConfigFile.scala      |  3 ++-
 .../docspell/restserver/RestAppImpl.scala     |  5 +++--
 .../docspell/restserver/RestServer.scala      |  8 +++++---
 .../restserver/routes/UploadRoutes.scala      |  7 ++++---
 project/Dependencies.scala                    |  4 ++--
 64 files changed, 224 insertions(+), 150 deletions(-)

diff --git a/build.sbt b/build.sbt
index 2d9057ac..4c9db62f 100644
--- a/build.sbt
+++ b/build.sbt
@@ -16,7 +16,7 @@ val scalafixSettings = Seq(
 
 val sharedSettings = Seq(
   organization := "com.github.eikek",
-  scalaVersion := "2.13.10",
+  scalaVersion := "2.13.12",
   organizationName := "Eike K. & Contributors",
   licenses += ("AGPL-3.0-or-later", url(
     "https://spdx.org/licenses/AGPL-3.0-or-later.html"
diff --git a/modules/addonlib/src/main/scala/docspell/addons/AddonArchive.scala b/modules/addonlib/src/main/scala/docspell/addons/AddonArchive.scala
index e12d8354..d4a367be 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/AddonArchive.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/AddonArchive.scala
@@ -19,7 +19,7 @@ final case class AddonArchive(url: LenientUri, name: String, version: String) {
   def nameAndVersion: String =
     s"$name-$version"
 
-  def extractTo[F[_]: Async](
+  def extractTo[F[_]: Async: Files](
       reader: UrlReader[F],
       directory: Path,
       withSubdir: Boolean = true,
@@ -48,7 +48,7 @@ final case class AddonArchive(url: LenientUri, name: String, version: String) {
   /** Read meta either from the given directory or extract the url to find the metadata
     * file to read
     */
-  def readMeta[F[_]: Async](
+  def readMeta[F[_]: Async: Files](
       urlReader: UrlReader[F],
       directory: Option[Path] = None
   ): F[AddonMeta] =
@@ -58,7 +58,7 @@ final case class AddonArchive(url: LenientUri, name: String, version: String) {
 }
 
 object AddonArchive {
-  def read[F[_]: Async](
+  def read[F[_]: Async: Files](
       url: LenientUri,
       urlReader: UrlReader[F],
       extractDir: Option[Path] = None
@@ -69,7 +69,7 @@ object AddonArchive {
       .map(m => addon.copy(name = m.meta.name, version = m.meta.version))
   }
 
-  def dockerAndFlakeExists[F[_]: Async](
+  def dockerAndFlakeExists[F[_]: Async: Files](
       archive: Either[Path, Stream[F, Byte]]
   ): F[(Boolean, Boolean)] = {
     val files = Files[F]
diff --git a/modules/addonlib/src/main/scala/docspell/addons/AddonExecutor.scala b/modules/addonlib/src/main/scala/docspell/addons/AddonExecutor.scala
index 79a496ca..e79581d7 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/AddonExecutor.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/AddonExecutor.scala
@@ -29,7 +29,7 @@ trait AddonExecutor[F[_]] {
 
 object AddonExecutor {
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: AddonExecutorConfig,
       urlReader: UrlReader[F]
   ): AddonExecutor[F] =
@@ -104,7 +104,7 @@ object AddonExecutor {
         } yield result
     }
 
-  def selectRunner[F[_]: Async](
+  def selectRunner[F[_]: Async: Files](
       cfg: AddonExecutorConfig,
       meta: AddonMeta,
       addonDir: Path
diff --git a/modules/addonlib/src/main/scala/docspell/addons/AddonMeta.scala b/modules/addonlib/src/main/scala/docspell/addons/AddonMeta.scala
index e68917d6..c6ad5fb6 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/AddonMeta.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/AddonMeta.scala
@@ -50,7 +50,7 @@ case class AddonMeta(
     * inspecting the archive to return defaults when the addon isn't declaring it in the
     * descriptor.
     */
-  def enabledTypes[F[_]: Async](
+  def enabledTypes[F[_]: Async: Files](
       archive: Either[Path, Stream[F, Byte]]
   ): F[List[RunnerType]] =
     for {
@@ -207,7 +207,7 @@ object AddonMeta {
       )
   }
 
-  def findInZip[F[_]: Async](zipFile: Stream[F, Byte]): F[AddonMeta] = {
+  def findInZip[F[_]: Async: Files](zipFile: Stream[F, Byte]): F[AddonMeta] = {
     val logger = docspell.logging.getLogger[F]
     val fail: F[AddonMeta] = Async[F].raiseError(
       new FileNotFoundException(
diff --git a/modules/addonlib/src/main/scala/docspell/addons/AddonRunner.scala b/modules/addonlib/src/main/scala/docspell/addons/AddonRunner.scala
index 75efe4a2..32c40bfe 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/AddonRunner.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/AddonRunner.scala
@@ -10,6 +10,7 @@ import cats.Applicative
 import cats.effect._
 import cats.syntax.all._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.addons.runner._
 import docspell.common.exec.Env
@@ -26,7 +27,9 @@ trait AddonRunner[F[_]] {
 }
 
 object AddonRunner {
-  def forType[F[_]: Async](cfg: AddonExecutorConfig)(rt: RunnerType) =
+  def forType[F[_]: Async: Files](
+      cfg: AddonExecutorConfig
+  )(rt: RunnerType): AddonRunner[F] =
     rt match {
       case RunnerType.NixFlake => NixFlakeRunner[F](cfg)
       case RunnerType.Docker   => DockerRunner[F](cfg)
@@ -38,9 +41,9 @@ object AddonRunner {
 
   def pure[F[_]: Applicative](result: AddonResult): AddonRunner[F] =
     new AddonRunner[F] {
-      val runnerType = Nil
+      val runnerType: List[RunnerType] = Nil
 
-      def run(logger: Logger[F], env: Env, ctx: Context) =
+      def run(logger: Logger[F], env: Env, ctx: Context): F[AddonResult] =
         Applicative[F].pure(result)
     }
 
@@ -50,9 +53,9 @@ object AddonRunner {
       case a :: Nil => a
       case _ =>
         new AddonRunner[F] {
-          val runnerType = runners.flatMap(_.runnerType).distinct
+          val runnerType: List[RunnerType] = runners.flatMap(_.runnerType).distinct
 
-          def run(logger: Logger[F], env: Env, ctx: Context) =
+          def run(logger: Logger[F], env: Env, ctx: Context): F[AddonResult] =
             Stream
               .emits(runners)
               .evalTap(r =>
diff --git a/modules/addonlib/src/main/scala/docspell/addons/runner/NixFlakeRunner.scala b/modules/addonlib/src/main/scala/docspell/addons/runner/NixFlakeRunner.scala
index ce12c73c..b255c256 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/runner/NixFlakeRunner.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/runner/NixFlakeRunner.scala
@@ -18,7 +18,7 @@ import docspell.common.Duration
 import docspell.common.exec._
 import docspell.logging.Logger
 
-final class NixFlakeRunner[F[_]: Async](cfg: NixFlakeRunner.Config)
+final class NixFlakeRunner[F[_]: Async: Files](cfg: NixFlakeRunner.Config)
     extends AddonRunner[F] {
 
   val runnerType = List(RunnerType.NixFlake)
@@ -104,7 +104,7 @@ final class NixFlakeRunner[F[_]: Async](cfg: NixFlakeRunner.Config)
 }
 
 object NixFlakeRunner {
-  def apply[F[_]: Async](cfg: AddonExecutorConfig): NixFlakeRunner[F] =
+  def apply[F[_]: Async: Files](cfg: AddonExecutorConfig): NixFlakeRunner[F] =
     new NixFlakeRunner[F](Config(cfg.nixRunner, cfg.nspawn, cfg.runTimeout))
 
   case class Config(
diff --git a/modules/addonlib/src/main/scala/docspell/addons/runner/RunnerUtil.scala b/modules/addonlib/src/main/scala/docspell/addons/runner/RunnerUtil.scala
index e404b442..7f44dcea 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/runner/RunnerUtil.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/runner/RunnerUtil.scala
@@ -45,7 +45,7 @@ private[addons] object RunnerUtil {
     * expected to be relative to the `ctx.baseDir`. Additional arguments and environment
     * variables are added as configured in the addon.
     */
-  def runInContainer[F[_]: Async](
+  def runInContainer[F[_]: Async: Files](
       logger: Logger[F],
       cfg: AddonExecutorConfig.NSpawn,
       ctx: Context
diff --git a/modules/addonlib/src/main/scala/docspell/addons/runner/TrivialRunner.scala b/modules/addonlib/src/main/scala/docspell/addons/runner/TrivialRunner.scala
index 31ac2694..9ec4edbd 100644
--- a/modules/addonlib/src/main/scala/docspell/addons/runner/TrivialRunner.scala
+++ b/modules/addonlib/src/main/scala/docspell/addons/runner/TrivialRunner.scala
@@ -19,10 +19,12 @@ import docspell.common.Duration
 import docspell.common.exec.{Args, Env, SysCmd}
 import docspell.logging.Logger
 
-final class TrivialRunner[F[_]: Async](cfg: TrivialRunner.Config) extends AddonRunner[F] {
+final class TrivialRunner[F[_]: Async: Files](cfg: TrivialRunner.Config)
+    extends AddonRunner[F] {
   private val sync = Async[F]
   private val files = Files[F]
-  implicit val andMonoid: Monoid[Boolean] = Monoid.instance[Boolean](true, _ && _)
+  implicit val andMonoid: Monoid[Boolean] =
+    Monoid.instance[Boolean](emptyValue = true, _ && _)
 
   private val executeBits = PosixPermissions(
     OwnerExecute,
@@ -34,13 +36,13 @@ final class TrivialRunner[F[_]: Async](cfg: TrivialRunner.Config) extends AddonR
     OthersRead
   )
 
-  val runnerType = List(RunnerType.Trivial)
+  val runnerType: List[RunnerType] = List(RunnerType.Trivial)
 
   def run(
       logger: Logger[F],
       env: Env,
       ctx: Context
-  ) = {
+  ): F[AddonResult] = {
     val binaryPath = ctx.meta.runner
       .flatMap(_.trivial)
       .map(_.exec)
@@ -71,7 +73,7 @@ final class TrivialRunner[F[_]: Async](cfg: TrivialRunner.Config) extends AddonR
 }
 
 object TrivialRunner {
-  def apply[F[_]: Async](cfg: AddonExecutorConfig): TrivialRunner[F] =
+  def apply[F[_]: Async: Files](cfg: AddonExecutorConfig): TrivialRunner[F] =
     new TrivialRunner[F](Config(cfg.nspawn, cfg.runTimeout))
 
   case class Config(nspawn: NSpawn, timeout: Duration)
diff --git a/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala b/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala
index ca92b377..0ab5e975 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala
@@ -9,6 +9,7 @@ package docspell.analysis
 import cats.Applicative
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.classifier.{StanfordTextClassifier, TextClassifier}
 import docspell.analysis.contact.Contact
@@ -36,7 +37,7 @@ object TextAnalyser {
       labels ++ dates.map(dl => dl.label.copy(label = dl.date.toString))
   }
 
-  def create[F[_]: Async](cfg: TextAnalysisConfig): Resource[F, TextAnalyser[F]] =
+  def create[F[_]: Async: Files](cfg: TextAnalysisConfig): Resource[F, TextAnalyser[F]] =
     Resource
       .eval(Nlp(cfg.nlpConfig))
       .map(stanfordNer =>
@@ -83,7 +84,7 @@ object TextAnalyser {
 
   /** Provides the nlp pipeline based on the configuration. */
   private object Nlp {
-    def apply[F[_]: Async](
+    def apply[F[_]: Async: Files](
         cfg: TextAnalysisConfig.NlpConfig
     ): F[Input[F] => F[Vector[NerLabel]]] = {
       val log = docspell.logging.getLogger[F]
diff --git a/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala b/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala
index 9542d3bb..c9e1e064 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala
@@ -21,7 +21,7 @@ import docspell.logging.Logger
 
 import edu.stanford.nlp.classify.ColumnDataClassifier
 
-final class StanfordTextClassifier[F[_]: Async](cfg: TextClassifierConfig)
+final class StanfordTextClassifier[F[_]: Async: Files](cfg: TextClassifierConfig)
     extends TextClassifier[F] {
 
   def trainClassifier[A](
diff --git a/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala b/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala
index 28f9490b..03db7afb 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala
@@ -11,6 +11,7 @@ import scala.concurrent.duration.{Duration => _, _}
 import cats.effect.Ref
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.NlpSettings
 import docspell.common._
@@ -32,7 +33,7 @@ trait PipelineCache[F[_]] {
 object PipelineCache {
   private[this] val logger = docspell.logging.unsafeLogger
 
-  def apply[F[_]: Async](clearInterval: Duration)(
+  def apply[F[_]: Async: Files](clearInterval: Duration)(
       creator: NlpSettings => Annotator[F],
       release: F[Unit]
   ): F[PipelineCache[F]] = {
@@ -44,7 +45,7 @@ object PipelineCache {
     } yield new Impl[F](data, creator, cacheClear)
   }
 
-  final private class Impl[F[_]: Async](
+  final private class Impl[F[_]: Async: Files](
       data: Ref[F, Map[String, Entry[Annotator[F]]]],
       creator: NlpSettings => Annotator[F],
       cacheClear: CacheClearing[F]
diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
index 365cdd00..8ebfa2ae 100644
--- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
+++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
@@ -7,6 +7,7 @@
 package docspell.backend
 
 import cats.effect._
+import fs2.io.file.Files
 
 import docspell.backend.BackendCommands.EventContext
 import docspell.backend.auth.Login
@@ -65,7 +66,7 @@ trait BackendApp[F[_]] {
 
 object BackendApp {
 
-  def create[F[_]: Async](
+  def create[F[_]: Async: Files](
       cfg: Config,
       store: Store[F],
       javaEmil: Emil[F],
diff --git a/modules/backend/src/main/scala/docspell/backend/joex/AddonOps.scala b/modules/backend/src/main/scala/docspell/backend/joex/AddonOps.scala
index 3fe95980..1cfa2a9e 100644
--- a/modules/backend/src/main/scala/docspell/backend/joex/AddonOps.scala
+++ b/modules/backend/src/main/scala/docspell/backend/joex/AddonOps.scala
@@ -9,6 +9,7 @@ package docspell.backend.joex
 import cats.data.OptionT
 import cats.effect._
 import cats.syntax.all._
+import fs2.io.file.Files
 
 import docspell.addons._
 import docspell.backend.joex.AddonOps.{AddonRunConfigRef, ExecResult}
@@ -98,7 +99,7 @@ object AddonOps {
       )
   }
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: AddonEnvConfig,
       store: Store[F],
       cmdRunner: BackendCommandRunner[F, Unit],
@@ -160,7 +161,10 @@ object AddonOps {
           execRes = ExecResult(List(result), List(runCfg))
         } yield execRes
 
-      def createMiddleware(custom: Middleware[F], runCfg: AddonRunConfigRef) = for {
+      def createMiddleware(
+          custom: Middleware[F],
+          runCfg: AddonRunConfigRef
+      ): F[Middleware[F]] = for {
         dscMW <- prepare.createDscEnv(runCfg, cfg.executorConfig.runTimeout)
         mm = dscMW >> custom >> prepare.logResult(logger, runCfg) >> Middleware
           .ephemeralRun[F]
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/AddonValidate.scala b/modules/backend/src/main/scala/docspell/backend/ops/AddonValidate.scala
index 73e783ac..9863d8fb 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/AddonValidate.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/AddonValidate.scala
@@ -10,7 +10,7 @@ import cats.data.EitherT
 import cats.effect._
 import cats.syntax.all._
 import fs2.Stream
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.addons.{AddonMeta, RunnerType}
 import docspell.backend.Config
@@ -21,7 +21,7 @@ import docspell.joexapi.model.AddonSupport
 import docspell.store.Store
 import docspell.store.records.RAddonArchive
 
-final class AddonValidate[F[_]: Async](
+final class AddonValidate[F[_]: Async: Files](
     cfg: Config.Addons,
     store: Store[F],
     joexOps: OJoex[F]
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OAddons.scala b/modules/backend/src/main/scala/docspell/backend/ops/OAddons.scala
index 4426658b..04e83983 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OAddons.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OAddons.scala
@@ -9,6 +9,7 @@ package docspell.backend.ops
 import cats.data.{EitherT, NonEmptyList, OptionT}
 import cats.effect._
 import cats.syntax.all._
+import fs2.io.file.Files
 
 import docspell.addons.{AddonMeta, AddonTriggerType}
 import docspell.backend.ops.AddonValidationError._
@@ -129,7 +130,7 @@ object OAddons {
     def failure[A](error: AddonValidationError): AddonValidationResult[A] = Left(error)
   }
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: Config.Addons,
       store: Store[F],
       userTasks: UserTaskStore[F],
diff --git a/modules/common/src/main/scala/docspell/common/Binary.scala b/modules/common/src/main/scala/docspell/common/Binary.scala
index 409ee6d4..5fd83e2f 100644
--- a/modules/common/src/main/scala/docspell/common/Binary.scala
+++ b/modules/common/src/main/scala/docspell/common/Binary.scala
@@ -39,7 +39,7 @@ final case class Binary[F[_]](name: String, mime: MimeType, data: Stream[F, Byte
 
 object Binary {
 
-  def apply[F[_]: Async](file: Path): Binary[F] =
+  def apply[F[_]: Files](file: Path): Binary[F] =
     Binary(file.fileName.toString, Files[F].readAll(file))
 
   def apply[F[_]](name: String, data: Stream[F, Byte]): Binary[F] =
@@ -74,11 +74,11 @@ object Binary {
     data.chunks.map(_.toByteVector).compile.fold(ByteVector.empty)((r, e) => r ++ e)
 
   /** Convert paths into `Binary`s */
-  def toBinary[F[_]: Async]: Pipe[F, Path, Binary[F]] =
+  def toBinary[F[_]: Files]: Pipe[F, Path, Binary[F]] =
     _.map(Binary[F](_))
 
   /** Save one or more binaries to a target directory. */
-  def saveTo[F[_]: Async](
+  def saveTo[F[_]: Async: Files](
       logger: Logger[F],
       targetDir: Path
   ): Pipe[F, Binary[F], Path] =
diff --git a/modules/common/src/main/scala/docspell/common/util/File.scala b/modules/common/src/main/scala/docspell/common/util/File.scala
index 679c99ec..f31fd8ae 100644
--- a/modules/common/src/main/scala/docspell/common/util/File.scala
+++ b/modules/common/src/main/scala/docspell/common/util/File.scala
@@ -72,6 +72,6 @@ object File {
       .drain
       .map(_ => file)
 
-  def readJson[F[_]: Async, A](file: Path)(implicit d: Decoder[A]): F[A] =
+  def readJson[F[_]: Async: Files, A](file: Path)(implicit d: Decoder[A]): F[A] =
     readText[F](file).map(parser.decode[A]).rethrow
 }
diff --git a/modules/common/src/main/scala/docspell/common/util/Zip.scala b/modules/common/src/main/scala/docspell/common/util/Zip.scala
index 335ed0b5..ca89ba10 100644
--- a/modules/common/src/main/scala/docspell/common/util/Zip.scala
+++ b/modules/common/src/main/scala/docspell/common/util/Zip.scala
@@ -7,7 +7,7 @@
 package docspell.common.util
 
 import cats.effect._
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 import fs2.{Pipe, Stream}
 
 import docspell.common.Glob
@@ -33,9 +33,9 @@ trait Zip[F[_]] {
 }
 
 object Zip {
-  val defaultChunkSize = 64 * 1024
+  private val defaultChunkSize = 64 * 1024
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       logger: Option[Logger[F]] = None,
       tempDir: Option[Path] = None
   ): Zip[F] =
diff --git a/modules/common/src/main/scala/docspell/common/util/ZipImpl.scala b/modules/common/src/main/scala/docspell/common/util/ZipImpl.scala
index be65eee2..2260b0e8 100644
--- a/modules/common/src/main/scala/docspell/common/util/ZipImpl.scala
+++ b/modules/common/src/main/scala/docspell/common/util/ZipImpl.scala
@@ -22,7 +22,7 @@ import fs2.{Chunk, Pipe, Stream}
 import docspell.common.Glob
 import docspell.logging.Logger
 
-final private class ZipImpl[F[_]: Async](
+final private class ZipImpl[F[_]: Async: Files](
     log: Option[Logger[F]],
     tempDir: Option[Path]
 ) extends Zip[F] {
diff --git a/modules/config/src/main/scala/docspell/config/ConfigFactory.scala b/modules/config/src/main/scala/docspell/config/ConfigFactory.scala
index 3522a48e..6af72e37 100644
--- a/modules/config/src/main/scala/docspell/config/ConfigFactory.scala
+++ b/modules/config/src/main/scala/docspell/config/ConfigFactory.scala
@@ -27,7 +27,10 @@ object ConfigFactory {
     *   1. if no file is found, read the config from environment variables falling back to
     *      the default config
     */
-  def default[F[_]: Async, C: ClassTag: ConfigReader](logger: Logger[F], atPath: String)(
+  def default[F[_]: Async: Files, C: ClassTag: ConfigReader](
+      logger: Logger[F],
+      atPath: String
+  )(
       args: List[String],
       validation: Validation[C]
   ): F[C] =
@@ -74,7 +77,7 @@ object ConfigFactory {
   /** Uses the first argument as a path to the config file. If it is specified but the
     * file doesn't exist, an exception is thrown.
     */
-  private def findFileFromArgs[F[_]: Async](args: List[String]): F[Option[Path]] =
+  private def findFileFromArgs[F[_]: Async: Files](args: List[String]): F[Option[Path]] =
     args.headOption
       .map(Path.apply)
       .traverse(p =>
@@ -89,7 +92,7 @@ object ConfigFactory {
     * to giving the file as argument, it is not an error to specify a non-existing file
     * via a system property.
     */
-  private def checkSystemProperty[F[_]: Async]: OptionT[F, Path] =
+  private def checkSystemProperty[F[_]: Async: Files]: OptionT[F, Path] =
     for {
       cf <- OptionT(
         Sync[F].delay(
diff --git a/modules/convert/src/main/scala/docspell/convert/Conversion.scala b/modules/convert/src/main/scala/docspell/convert/Conversion.scala
index ab9cce88..6ab0c149 100644
--- a/modules/convert/src/main/scala/docspell/convert/Conversion.scala
+++ b/modules/convert/src/main/scala/docspell/convert/Conversion.scala
@@ -11,6 +11,7 @@ import java.nio.charset.StandardCharsets
 import cats.effect._
 import cats.implicits._
 import fs2._
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.convert.ConversionResult.Handler
@@ -32,7 +33,7 @@ trait Conversion[F[_]] {
 
 object Conversion {
 
-  def create[F[_]: Async](
+  def create[F[_]: Async: Files](
       cfg: ConvertConfig,
       sanitizeHtml: SanitizeHtml,
       additionalPasswords: List[Password],
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala b/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala
index 8f0f9e11..5f1253f5 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala
@@ -19,7 +19,7 @@ import docspell.logging.Logger
 
 private[extern] object ExternConv {
 
-  def toPDF[F[_]: Async, A](
+  def toPDF[F[_]: Async: Files, A](
       name: String,
       cmdCfg: SystemCommand.Config,
       wd: Path,
@@ -71,7 +71,7 @@ private[extern] object ExternConv {
           handler.run(ConversionResult.failure(ex))
       }
 
-  def readResult[F[_]: Async](
+  def readResult[F[_]: Async: Files](
       chunkSize: Int,
       logger: Logger[F]
   )(out: Path, result: SystemCommand.Result): F[ConversionResult[F]] =
@@ -99,7 +99,7 @@ private[extern] object ExternConv {
           .pure[F]
     }
 
-  def readResultTesseract[F[_]: Async](
+  def readResultTesseract[F[_]: Async: Files](
       outPrefix: String,
       chunkSize: Int,
       logger: Logger[F]
@@ -127,7 +127,7 @@ private[extern] object ExternConv {
     }
   }
 
-  private def storeDataToFile[F[_]: Async](
+  private def storeDataToFile[F[_]: Async: Files](
       name: String,
       logger: Logger[F],
       inFile: Path
@@ -146,7 +146,7 @@ private[extern] object ExternConv {
     logger.debug(s"$name stdout: ${result.stdout}") *>
       logger.debug(s"$name stderr: ${result.stderr}")
 
-  private def storeFile[F[_]: Async](
+  private def storeFile[F[_]: Async: Files](
       in: Stream[F, Byte],
       target: Path
   ): F[Unit] =
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala b/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala
index d133b4dd..1fc778ef 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala
@@ -8,7 +8,7 @@ package docspell.convert.extern
 
 import cats.effect._
 import fs2.Stream
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.common._
 import docspell.convert.ConversionResult
@@ -17,7 +17,7 @@ import docspell.logging.Logger
 
 object OcrMyPdf {
 
-  def toPDF[F[_]: Async, A](
+  def toPDF[F[_]: Async: Files, A](
       cfg: OcrMyPdfConfig,
       lang: Language,
       chunkSize: Int,
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala b/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala
index bd2ae95d..95875f8f 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala
@@ -8,7 +8,7 @@ package docspell.convert.extern
 
 import cats.effect._
 import fs2.Stream
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.common._
 import docspell.convert.ConversionResult
@@ -17,7 +17,7 @@ import docspell.logging.Logger
 
 object Tesseract {
 
-  def toPDF[F[_]: Async, A](
+  def toPDF[F[_]: Async: Files, A](
       cfg: TesseractConfig,
       lang: Language,
       chunkSize: Int,
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala b/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala
index efe0efa7..cbe0db87 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala
@@ -8,7 +8,7 @@ package docspell.convert.extern
 
 import cats.effect._
 import fs2.Stream
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.common._
 import docspell.convert.ConversionResult
@@ -17,7 +17,7 @@ import docspell.logging.Logger
 
 object Unoconv {
 
-  def toPDF[F[_]: Async, A](
+  def toPDF[F[_]: Async: Files, A](
       cfg: UnoconvConfig,
       chunkSize: Int,
       logger: Logger[F]
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Weasyprint.scala b/modules/convert/src/main/scala/docspell/convert/extern/Weasyprint.scala
index ba1d6ce7..2470d0fe 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/Weasyprint.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/Weasyprint.scala
@@ -10,7 +10,7 @@ import java.nio.charset.Charset
 
 import cats.effect._
 import cats.implicits._
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 import fs2.{Chunk, Stream}
 
 import docspell.common._
@@ -20,7 +20,7 @@ import docspell.logging.Logger
 
 object Weasyprint {
 
-  def toPDF[F[_]: Async, A](
+  def toPDF[F[_]: Async: Files, A](
       cfg: WeasyprintConfig,
       chunkSize: Int,
       charset: Charset,
@@ -46,7 +46,7 @@ object Weasyprint {
     )
 
     ExternConv
-      .toPDF[F, A]("weasyprint", cmdCfg, cfg.workingDir, true, logger, reader)(
+      .toPDF[F, A]("weasyprint", cmdCfg, cfg.workingDir, useStdin = true, logger, reader)(
         inSane,
         handler
       )
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala b/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala
index d3ed16c0..04e973fe 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala
@@ -10,7 +10,7 @@ import java.nio.charset.Charset
 
 import cats.effect._
 import cats.implicits._
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 import fs2.{Chunk, Stream}
 
 import docspell.common._
@@ -20,7 +20,7 @@ import docspell.logging.Logger
 
 object WkHtmlPdf {
 
-  def toPDF[F[_]: Async, A](
+  def toPDF[F[_]: Async: Files, A](
       cfg: WkHtmlPdfConfig,
       chunkSize: Int,
       charset: Charset,
@@ -46,7 +46,14 @@ object WkHtmlPdf {
     )
 
     ExternConv
-      .toPDF[F, A]("wkhtmltopdf", cmdCfg, cfg.workingDir, true, logger, reader)(
+      .toPDF[F, A](
+        "wkhtmltopdf",
+        cmdCfg,
+        cfg.workingDir,
+        useStdin = true,
+        logger,
+        reader
+      )(
         inSane,
         handler
       )
diff --git a/modules/extract/src/main/scala/docspell/extract/Extraction.scala b/modules/extract/src/main/scala/docspell/extract/Extraction.scala
index c1a27023..b57b9f99 100644
--- a/modules/extract/src/main/scala/docspell/extract/Extraction.scala
+++ b/modules/extract/src/main/scala/docspell/extract/Extraction.scala
@@ -9,6 +9,7 @@ package docspell.extract
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.extract.internal.Text
@@ -32,7 +33,7 @@ trait Extraction[F[_]] {
 
 object Extraction {
 
-  def create[F[_]: Async](
+  def create[F[_]: Async: Files](
       logger: Logger[F],
       cfg: ExtractConfig
   ): Extraction[F] =
diff --git a/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala b/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala
index 400a1a4e..8d6e7208 100644
--- a/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala
+++ b/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala
@@ -9,6 +9,7 @@ package docspell.extract
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.common.Language
 import docspell.extract.internal.Text
@@ -24,7 +25,7 @@ object PdfExtract {
       Result(t._1, t._2)
   }
 
-  def get[F[_]: Async](
+  def get[F[_]: Async: Files](
       in: Stream[F, Byte],
       lang: Language,
       stripMinLen: Int,
diff --git a/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala b/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala
index f39e4b0b..b0828201 100644
--- a/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala
+++ b/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala
@@ -8,7 +8,7 @@ package docspell.extract.ocr
 
 import cats.effect._
 import fs2.Stream
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.common._
 import docspell.common.util.File
@@ -17,7 +17,7 @@ import docspell.logging.Logger
 object Ocr {
 
   /** Extract the text of all pages in the given pdf file. */
-  def extractPdf[F[_]: Async](
+  def extractPdf[F[_]: Async: Files](
       pdf: Stream[F, Byte],
       logger: Logger[F],
       lang: String,
@@ -40,7 +40,7 @@ object Ocr {
   ): Stream[F, String] =
     runTesseractStdin(img, logger, lang, config)
 
-  def extractPdFFile[F[_]: Async](
+  def extractPdFFile[F[_]: Async: Files](
       pdf: Path,
       logger: Logger[F],
       lang: String,
@@ -65,7 +65,7 @@ object Ocr {
   /** Run ghostscript to extract all pdf pages into tiff files. The files are stored to a
     * temporary location on disk and returned.
     */
-  private[extract] def runGhostscript[F[_]: Async](
+  private[extract] def runGhostscript[F[_]: Async: Files](
       pdf: Stream[F, Byte],
       cfg: OcrConfig,
       wd: Path,
@@ -91,7 +91,7 @@ object Ocr {
   /** Run ghostscript to extract all pdf pages into tiff files. The files are stored to a
     * temporary location on disk and returned.
     */
-  private[extract] def runGhostscriptFile[F[_]: Async](
+  private[extract] def runGhostscriptFile[F[_]: Async: Files](
       pdf: Path,
       ghostscript: SystemCommand.Config,
       wd: Path,
diff --git a/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala b/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala
index a08a2cee..0fc8123e 100644
--- a/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala
+++ b/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala
@@ -8,6 +8,7 @@ package docspell.extract.ocr
 
 import cats.effect._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.extract.internal.Text
@@ -16,7 +17,7 @@ import docspell.logging.Logger
 
 object TextExtract {
 
-  def extract[F[_]: Async](
+  def extract[F[_]: Async: Files](
       in: Stream[F, Byte],
       logger: Logger[F],
       lang: String,
@@ -24,7 +25,7 @@ object TextExtract {
   ): Stream[F, Text] =
     extractOCR(in, logger, lang, config)
 
-  def extractOCR[F[_]: Async](
+  def extractOCR[F[_]: Async: Files](
       in: Stream[F, Byte],
       logger: Logger[F],
       lang: String,
diff --git a/modules/files/src/main/scala/docspell/files/FileSupport.scala b/modules/files/src/main/scala/docspell/files/FileSupport.scala
index bdd19197..5f924445 100644
--- a/modules/files/src/main/scala/docspell/files/FileSupport.scala
+++ b/modules/files/src/main/scala/docspell/files/FileSupport.scala
@@ -7,7 +7,7 @@
 package docspell.files
 
 import cats.data.OptionT
-import cats.effect.{Async, Sync}
+import cats.effect.Sync
 import cats.syntax.all._
 import fs2.Pipe
 import fs2.io.file.{Files, Path}
@@ -39,7 +39,7 @@ trait FileSupport {
       TikaMimetype.detect[F](bin.data, hint).map(mt => bin.copy(mime = mt))
     }
 
-  def toBinaryWithMime[F[_]: Async]: Pipe[F, Path, Binary[F]] =
+  def toBinaryWithMime[F[_]: Sync: Files]: Pipe[F, Path, Binary[F]] =
     _.evalMap(file => file.mimeType.map(mt => Binary(file).copy(mime = mt)))
 }
 
diff --git a/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala b/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala
index f32378bb..059f28a7 100644
--- a/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala
+++ b/modules/joex/src/main/scala/docspell/joex/ConfigFile.scala
@@ -7,6 +7,7 @@
 package docspell.joex
 
 import cats.effect.Async
+import fs2.io.file.Files
 
 import docspell.config.Implicits._
 import docspell.config.{ConfigFactory, FtsType, Validation}
@@ -25,7 +26,7 @@ object ConfigFile {
   // IntelliJ is wrong, this is required
   import Implicits._
 
-  def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
+  def loadConfig[F[_]: Async: Files](args: List[String]): F[Config] = {
     val logger = docspell.logging.getLogger[F]
     ConfigFactory
       .default[F, Config](logger, "docspell.joex")(args, validate)
diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
index bf9e0137..ebf57d44 100644
--- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
+++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
@@ -9,6 +9,8 @@ package docspell.joex
 import cats.effect._
 import cats.implicits._
 import fs2.concurrent.SignallingRef
+import fs2.io.file.Files
+import fs2.io.net.Network
 
 import docspell.backend.MailAddressCodec
 import docspell.backend.joex.FindJobOwnerAccount
@@ -45,7 +47,7 @@ final class JoexAppImpl[F[_]: Async](
   def init: F[Unit] = {
     val run = scheduler.start.compile.drain
     val prun = periodicScheduler.start.compile.drain
-    val eventConsume = notificationMod.consumeAllEvents(2).compile.drain
+    val eventConsume = notificationMod.consumeAllEvents(maxConcurrent = 2).compile.drain
     for {
       _ <- scheduleBackgroundTasks
       _ <- Async[F].start(run)
@@ -62,7 +64,9 @@ final class JoexAppImpl[F[_]: Async](
     store.transact(RJobLog.findLogs(jobId))
 
   def initShutdown: F[Unit] =
-    periodicScheduler.shutdown *> scheduler.shutdown(false) *> termSignal.set(true)
+    periodicScheduler.shutdown *> scheduler.shutdown(cancelAll = false) *> termSignal.set(
+      true
+    )
 
   private def scheduleBackgroundTasks: F[Unit] =
     HouseKeepingTask
@@ -81,7 +85,8 @@ final class JoexAppImpl[F[_]: Async](
   private def scheduleEmptyTrashTasks: F[Unit] =
     store
       .transact(
-        REmptyTrashSetting.findForAllCollectives(OCollective.EmptyTrash.default, 50)
+        REmptyTrashSetting
+          .findForAllCollectives(OCollective.EmptyTrash.default, chunkSize = 50)
       )
       .evalMap { es =>
         val args = EmptyTrashArgs(es.cid, es.minAge)
@@ -98,7 +103,7 @@ final class JoexAppImpl[F[_]: Async](
 
 object JoexAppImpl extends MailAddressCodec {
 
-  def create[F[_]: Async](
+  def create[F[_]: Async: Files: Network](
       cfg: Config,
       termSignal: SignallingRef[F, Boolean],
       store: Store[F],
@@ -107,12 +112,14 @@ object JoexAppImpl extends MailAddressCodec {
       pools: Pools
   ): Resource[F, JoexApp[F]] =
     for {
-      joexLogger <- Resource.pure(docspell.logging.getLogger[F](s"joex-${cfg.appId.id}"))
+      joexLogger <- Resource.pure(
+        docspell.logging.getLogger[F](name = s"joex-${cfg.appId.id}")
+      )
       pubSubT = PubSubT(pubSub, joexLogger)
       javaEmil =
         JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug))
       notificationMod <- Resource.eval(
-        NotificationModuleImpl[F](store, javaEmil, httpClient, 200)
+        NotificationModuleImpl[F](store, javaEmil, httpClient, queueSize = 200)
       )
 
       jobStoreModule = JobStoreModuleBuilder(store)
diff --git a/modules/joex/src/main/scala/docspell/joex/JoexServer.scala b/modules/joex/src/main/scala/docspell/joex/JoexServer.scala
index e138ba38..e527f811 100644
--- a/modules/joex/src/main/scala/docspell/joex/JoexServer.scala
+++ b/modules/joex/src/main/scala/docspell/joex/JoexServer.scala
@@ -9,6 +9,8 @@ package docspell.joex
 import cats.effect._
 import fs2.Stream
 import fs2.concurrent.SignallingRef
+import fs2.io.file.Files
+import fs2.io.net.Network
 
 import docspell.backend.msg.Topics
 import docspell.common.Pools
@@ -32,7 +34,10 @@ object JoexServer {
       exitRef: Ref[F, ExitCode]
   )
 
-  def stream[F[_]: Async](cfg: Config, pools: Pools): Stream[F, Nothing] = {
+  def stream[F[_]: Async: Files: Network](
+      cfg: Config,
+      pools: Pools
+  ): Stream[F, Nothing] = {
 
     val app = for {
       signal <- Resource.eval(SignallingRef[F, Boolean](false))
diff --git a/modules/joex/src/main/scala/docspell/joex/JoexTasks.scala b/modules/joex/src/main/scala/docspell/joex/JoexTasks.scala
index 7114be6d..639dfa49 100644
--- a/modules/joex/src/main/scala/docspell/joex/JoexTasks.scala
+++ b/modules/joex/src/main/scala/docspell/joex/JoexTasks.scala
@@ -7,6 +7,8 @@
 package docspell.joex
 
 import cats.effect.{Async, Resource}
+import fs2.io.file.Files
+import fs2.io.net.Network
 
 import docspell.analysis.TextAnalyser
 import docspell.backend.BackendCommands
@@ -46,7 +48,7 @@ import docspell.store.Store
 import emil.Emil
 import org.http4s.client.Client
 
-final class JoexTasks[F[_]: Async](
+final class JoexTasks[F[_]: Async: Files: Network](
     cfg: Config,
     store: Store[F],
     itemOps: OItem[F],
@@ -257,7 +259,7 @@ final class JoexTasks[F[_]: Async](
 
 object JoexTasks {
 
-  def resource[F[_]: Async](
+  def resource[F[_]: Async: Files: Network](
       cfg: Config,
       pools: Pools,
       jobStoreModule: JobStoreModuleBuilder.Module[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/addon/GenericItemAddonTask.scala b/modules/joex/src/main/scala/docspell/joex/addon/GenericItemAddonTask.scala
index 37cc0ef7..bbe9e2eb 100644
--- a/modules/joex/src/main/scala/docspell/joex/addon/GenericItemAddonTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/addon/GenericItemAddonTask.scala
@@ -43,7 +43,7 @@ object GenericItemAddonTask extends LoggerExtension {
     "ITEM_PDF_JSON" -> pdfMetaJson
   )
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       ops: AddonOps[F],
       store: Store[F],
       trigger: AddonTriggerType,
@@ -57,7 +57,7 @@ object GenericItemAddonTask extends LoggerExtension {
       data
     )
 
-  def addonResult[F[_]: Async](
+  def addonResult[F[_]: Async: Files](
       ops: AddonOps[F],
       store: Store[F],
       trigger: AddonTriggerType,
@@ -73,7 +73,7 @@ object GenericItemAddonTask extends LoggerExtension {
       )
     }
 
-  def prepareItemData[F[_]: Async](
+  def prepareItemData[F[_]: Async: Files](
       logger: Logger[F],
       store: Store[F],
       data: ItemData,
diff --git a/modules/joex/src/main/scala/docspell/joex/addon/ItemAddonTask.scala b/modules/joex/src/main/scala/docspell/joex/addon/ItemAddonTask.scala
index 32f8bc7b..8dfb0a3a 100644
--- a/modules/joex/src/main/scala/docspell/joex/addon/ItemAddonTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/addon/ItemAddonTask.scala
@@ -9,6 +9,7 @@ package docspell.joex.addon
 import cats.data.OptionT
 import cats.effect._
 import cats.syntax.all._
+import fs2.io.file.Files
 
 import docspell.addons.AddonTriggerType
 import docspell.backend.joex.AddonOps
@@ -26,7 +27,10 @@ object ItemAddonTask extends AddonTaskExtension {
   def onCancel[F[_]]: Task[F, Args, Unit] =
     Task.log(_.warn(s"Cancelling ${name.id} task"))
 
-  def apply[F[_]: Async](ops: AddonOps[F], store: Store[F]): Task[F, Args, Result] =
+  def apply[F[_]: Async: Files](
+      ops: AddonOps[F],
+      store: Store[F]
+  ): Task[F, Args, Result] =
     Task { ctx =>
       (for {
         item <- OptionT(
diff --git a/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala b/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala
index 52f51ea9..0077b6e5 100644
--- a/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala
+++ b/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala
@@ -8,7 +8,7 @@ package docspell.joex.analysis
 
 import cats.effect._
 import cats.implicits._
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.analysis.split.TextSplitter
 import docspell.common._
@@ -39,7 +39,7 @@ object NerFile {
   private def jsonFilePath(directory: Path, collective: CollectiveId): Path =
     directory.resolve(s"${collective.value}.json")
 
-  def find[F[_]: Async](
+  def find[F[_]: Async: Files](
       collective: CollectiveId,
       directory: Path
   ): F[Option[NerFile]] = {
diff --git a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
index 8801bff3..327a725e 100644
--- a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
+++ b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
@@ -9,7 +9,7 @@ package docspell.joex.analysis
 import cats.effect._
 import cats.effect.std.Semaphore
 import cats.implicits._
-import fs2.io.file.Path
+import fs2.io.file.{Files, Path}
 
 import docspell.common._
 import docspell.common.util.File
@@ -32,7 +32,7 @@ object RegexNerFile {
 
   case class Config(maxEntries: Int, directory: Path, minTime: Duration)
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: Config,
       store: Store[F]
   ): Resource[F, RegexNerFile[F]] =
@@ -41,7 +41,7 @@ object RegexNerFile {
       writer <- Resource.eval(Semaphore(1))
     } yield new Impl[F](cfg.copy(directory = dir), store, writer)
 
-  final private class Impl[F[_]: Async](
+  final private class Impl[F[_]: Async: Files](
       cfg: Config,
       store: Store[F],
       writer: Semaphore[F] // TODO allow parallelism per collective
diff --git a/modules/joex/src/main/scala/docspell/joex/download/DownloadZipTask.scala b/modules/joex/src/main/scala/docspell/joex/download/DownloadZipTask.scala
index 92856b9b..3d8b9000 100644
--- a/modules/joex/src/main/scala/docspell/joex/download/DownloadZipTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/download/DownloadZipTask.scala
@@ -10,6 +10,7 @@ import java.time.format.DateTimeFormatter
 
 import cats.effect._
 import cats.syntax.all._
+import fs2.io.file.Files
 import fs2.{Pipe, Stream}
 
 import docspell.backend.ops.ODownloadAll
@@ -28,7 +29,7 @@ object DownloadZipTask {
   def onCancel[F[_]]: Task[F, Args, Unit] =
     Task.log(_.warn(s"Cancelling ${DownloadZipArgs.taskName.id} task"))
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       chunkSize: Int,
       store: Store[F],
       downloadOps: ODownloadAll[F]
diff --git a/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala b/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala
index 7f9de5ae..26552511 100644
--- a/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala
@@ -8,6 +8,7 @@ package docspell.joex.hk
 
 import cats.effect._
 import cats.implicits._
+import fs2.io.net.Network
 
 import docspell.common._
 import docspell.logging.Logger
@@ -19,7 +20,7 @@ import org.http4s.client.Client
 import org.http4s.ember.client.EmberClientBuilder
 
 object CheckNodesTask {
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Network](
       cfg: HouseKeepingConfig.CheckNodes,
       store: Store[F]
   ): Task[F, Unit, CleanupResult] =
diff --git a/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala b/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala
index 3d7d88cd..519cfa03 100644
--- a/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala
@@ -8,6 +8,7 @@ package docspell.joex.hk
 
 import cats.effect._
 import cats.implicits._
+import fs2.io.net.Network
 
 import docspell.backend.ops.{ODownloadAll, OFileRepository}
 import docspell.common._
@@ -26,7 +27,7 @@ object HouseKeepingTask {
 
   val taskName: Ident = Ident.unsafe("housekeeping")
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Network](
       cfg: Config,
       store: Store[F],
       fileRepo: OFileRepository[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala b/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala
index 9812e619..92db0c19 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala
@@ -21,7 +21,7 @@ import docspell.store.records.RClassifierModel
 
 object Classify {
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       logger: Logger[F],
       workingDir: Path,
       store: Store[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala
index 06e9ad83..74dc4cdb 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala
@@ -9,6 +9,7 @@ package docspell.joex.learn
 import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.TextAnalyser
 import docspell.backend.ops.OCollective
@@ -28,7 +29,7 @@ object LearnClassifierTask {
   def onCancel[F[_]]: Task[F, Args, Unit] =
     Task.log(_.warn("Cancelling learn-classifier task"))
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: Config.TextAnalysis,
       store: Store[F],
       analyser: TextAnalyser[F]
@@ -37,7 +38,7 @@ object LearnClassifierTask {
       .flatMap(_ => learnItemEntities(cfg, store, analyser))
       .flatMap(_ => Task(_ => Sync[F].delay(System.gc())))
 
-  private def learnItemEntities[F[_]: Async](
+  private def learnItemEntities[F[_]: Async: Files](
       cfg: Config.TextAnalysis,
       store: Store[F],
       analyser: TextAnalyser[F]
@@ -56,7 +57,7 @@ object LearnClassifierTask {
       else ().pure[F]
     }
 
-  private def learnTags[F[_]: Async](
+  private def learnTags[F[_]: Async: Files](
       cfg: Config.TextAnalysis,
       store: Store[F],
       analyser: TextAnalyser[F]
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala
index d394b536..8015e11f 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala
@@ -10,6 +10,7 @@ import cats.data.Kleisli
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.analysis.TextAnalyser
 import docspell.analysis.classifier.TextClassifier.Data
@@ -18,7 +19,7 @@ import docspell.scheduler._
 import docspell.store.Store
 
 object LearnItemEntities {
-  def learnAll[F[_]: Async, A](
+  def learnAll[F[_]: Async: Files, A](
       analyser: TextAnalyser[F],
       store: Store[F],
       collective: CollectiveId,
@@ -32,7 +33,7 @@ object LearnItemEntities {
       .flatMap(_ => learnConcPerson(analyser, store, collective, maxItems, maxTextLen))
       .flatMap(_ => learnConcEquip(analyser, store, collective, maxItems, maxTextLen))
 
-  def learnCorrOrg[F[_]: Async, A](
+  def learnCorrOrg[F[_]: Async: Files, A](
       analyser: TextAnalyser[F],
       store: Store[F],
       collective: CollectiveId,
@@ -44,7 +45,7 @@ object LearnItemEntities {
       _ => SelectItems.forCorrOrg(store, collective, maxItems, maxTextLen)
     )
 
-  def learnCorrPerson[F[_]: Async, A](
+  def learnCorrPerson[F[_]: Async: Files, A](
       analyser: TextAnalyser[F],
       store: Store[F],
       collective: CollectiveId,
@@ -56,7 +57,7 @@ object LearnItemEntities {
       _ => SelectItems.forCorrPerson(store, collective, maxItems, maxTextLen)
     )
 
-  def learnConcPerson[F[_]: Async, A](
+  def learnConcPerson[F[_]: Async: Files, A](
       analyser: TextAnalyser[F],
       store: Store[F],
       collective: CollectiveId,
@@ -68,7 +69,7 @@ object LearnItemEntities {
       _ => SelectItems.forConcPerson(store, collective, maxItems, maxTextLen)
     )
 
-  def learnConcEquip[F[_]: Async, A](
+  def learnConcEquip[F[_]: Async: Files, A](
       analyser: TextAnalyser[F],
       store: Store[F],
       collective: CollectiveId,
@@ -80,7 +81,7 @@ object LearnItemEntities {
       _ => SelectItems.forConcEquip(store, collective, maxItems, maxTextLen)
     )
 
-  private def learn[F[_]: Async, A](
+  private def learn[F[_]: Async: Files, A](
       store: Store[F],
       analyser: TextAnalyser[F],
       collective: CollectiveId
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala
index 9745f2aa..55eecb90 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala
@@ -9,6 +9,7 @@ package docspell.joex.learn
 import cats.data.Kleisli
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.TextAnalyser
 import docspell.common._
@@ -18,7 +19,7 @@ import docspell.store.records.RClassifierSetting
 
 object LearnTags {
 
-  def learnTagCategory[F[_]: Async, A](
+  def learnTagCategory[F[_]: Async: Files, A](
       analyser: TextAnalyser[F],
       store: Store[F],
       collective: CollectiveId,
@@ -43,7 +44,10 @@ object LearnTags {
         )
     }
 
-  def learnAllTagCategories[F[_]: Async, A](analyser: TextAnalyser[F], store: Store[F])(
+  def learnAllTagCategories[F[_]: Async: Files, A](
+      analyser: TextAnalyser[F],
+      store: Store[F]
+  )(
       collective: CollectiveId,
       maxItems: Int,
       maxTextLen: Int
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala b/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala
index b3ff3261..6879dcb5 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala
@@ -18,7 +18,7 @@ import docspell.store.records.RClassifierModel
 
 object StoreClassifierModel {
 
-  def handleModel[F[_]: Async](
+  def handleModel[F[_]: Async: Files](
       store: Store[F],
       logger: Logger[F],
       collective: CollectiveId,
diff --git a/modules/joex/src/main/scala/docspell/joex/multiupload/MultiUploadArchiveTask.scala b/modules/joex/src/main/scala/docspell/joex/multiupload/MultiUploadArchiveTask.scala
index e1ff77a1..61a6537a 100644
--- a/modules/joex/src/main/scala/docspell/joex/multiupload/MultiUploadArchiveTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/multiupload/MultiUploadArchiveTask.scala
@@ -11,6 +11,7 @@ import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.backend.JobFactory
 import docspell.common._
@@ -35,7 +36,10 @@ import docspell.store.Store
 object MultiUploadArchiveTask {
   type Args = ProcessItemArgs
 
-  def apply[F[_]: Async](store: Store[F], jobStore: JobStore[F]): Task[F, Args, Result] =
+  def apply[F[_]: Async: Files](
+      store: Store[F],
+      jobStore: JobStore[F]
+  ): Task[F, Args, Result] =
     Task { ctx =>
       ctx.args.files
         .traverse { file =>
@@ -104,7 +108,7 @@ object MultiUploadArchiveTask {
       .map(_.mimetype.matches(MimeType.zip))
       .getOrElse(false)
 
-  private def extractZip[F[_]: Async](
+  private def extractZip[F[_]: Async: Files](
       store: Store[F],
       args: Args
   )(file: ProcessItemArgs.File): Stream[F, ProcessItemArgs] =
diff --git a/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala b/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala
index a1ce38fb..8fe5e62e 100644
--- a/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala
@@ -11,6 +11,7 @@ import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.convert.ConversionResult
@@ -35,9 +36,9 @@ object PdfConvTask {
       deriveEncoder[Args]
   }
 
-  val taskName = Ident.unsafe("pdf-files-migration")
+  val taskName: Ident = Ident.unsafe("pdf-files-migration")
 
-  def apply[F[_]: Async](cfg: Config, store: Store[F]): Task[F, Args, Unit] =
+  def apply[F[_]: Async: Files](cfg: Config, store: Store[F]): Task[F, Args, Unit] =
     Task { ctx =>
       for {
         _ <- ctx.logger.info(s"Converting pdf file ${ctx.args} using ocrmypdf")
@@ -89,7 +90,7 @@ object PdfConvTask {
     else none.pure[F]
   }
 
-  def convert[F[_]: Async](
+  def convert[F[_]: Async: Files](
       cfg: Config,
       ctx: Context[F, Args],
       store: Store[F],
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 648d6b29..6066c9fa 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala
@@ -11,6 +11,7 @@ import cats.data.{Kleisli, OptionT}
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.convert.ConversionResult.Handler
@@ -35,7 +36,7 @@ import docspell.store.records._
 object ConvertPdf {
   type Args = ProcessItemArgs
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: ConvertConfig,
       store: Store[F],
       item: ItemData
@@ -76,7 +77,7 @@ object ConvertPdf {
       .map(_.mimetype)
       .getOrElse(MimeType.octetStream)
 
-  def convertSafe[F[_]: Async](
+  def convertSafe[F[_]: Async: Files](
       cfg: ConvertConfig,
       sanitizeHtml: SanitizeHtml,
       ctx: Context[F, Args],
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala b/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala
index 410e31bb..8baa488a 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala
@@ -14,6 +14,7 @@ import cats.implicits._
 import cats.kernel.Monoid
 import cats.kernel.Order
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.common.util.Zip
@@ -35,12 +36,12 @@ import emil.Mail
 object ExtractArchive {
   type Args = ProcessItemArgs
 
-  def apply[F[_]: Async](store: Store[F])(
+  def apply[F[_]: Async: Files](store: Store[F])(
       item: ItemData
   ): Task[F, Args, ItemData] =
     multiPass(store, item, None).map(_._2)
 
-  def multiPass[F[_]: Async](
+  def multiPass[F[_]: Async: Files](
       store: Store[F],
       item: ItemData,
       archive: Option[RAttachmentArchive]
@@ -50,7 +51,7 @@ object ExtractArchive {
       else multiPass(store, t._2, t._1)
     }
 
-  def singlePass[F[_]: Async](
+  def singlePass[F[_]: Async: Files](
       store: Store[F],
       item: ItemData,
       archive: Option[RAttachmentArchive]
@@ -91,7 +92,7 @@ object ExtractArchive {
       .map(_.mimetype)
       .getOrElse(MimeType.octetStream)
 
-  def extractSafe[F[_]: Async](
+  def extractSafe[F[_]: Async: Files](
       ctx: Context[F, Args],
       store: Store[F],
       archive: Option[RAttachmentArchive]
@@ -137,7 +138,7 @@ object ExtractArchive {
         } yield extracted.copy(files = extracted.files.filter(_.id != ra.id))
     }
 
-  def extractZip[F[_]: Async](
+  def extractZip[F[_]: Async: Files](
       ctx: Context[F, Args],
       store: Store[F],
       archive: Option[RAttachmentArchive]
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala b/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala
index b5fc216e..8e361a2b 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala
@@ -10,6 +10,7 @@ import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.analysis.TextAnalyser
 import docspell.backend.joex.AddonOps
@@ -36,7 +37,7 @@ object ItemHandler {
       }
     )
 
-  def newItem[F[_]: Async](
+  def newItem[F[_]: Async: Files](
       cfg: Config,
       store: Store[F],
       itemOps: OItem[F],
@@ -82,7 +83,7 @@ object ItemHandler {
   def isLastRetry[F[_]]: Task[F, Args, Boolean] =
     Task(_.isLastRetry)
 
-  def safeProcess[F[_]: Async](
+  def safeProcess[F[_]: Async: Files](
       cfg: Config,
       store: Store[F],
       itemOps: OItem[F],
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 836a8062..9b52bcb9 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala
@@ -8,6 +8,7 @@ package docspell.joex.process
 
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.addons.AddonTriggerType
 import docspell.analysis.TextAnalyser
@@ -22,7 +23,7 @@ import docspell.store.Store
 
 object ProcessItem {
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: Config,
       itemOps: OItem[F],
       fts: FtsClient[F],
@@ -40,7 +41,7 @@ object ProcessItem {
       .flatMap(RemoveEmptyItem(itemOps))
       .flatMap(RunAddons(addonOps, store, AddonTriggerType.FinalProcessItem))
 
-  def processAttachments[F[_]: Async](
+  def processAttachments[F[_]: Async: Files](
       cfg: Config,
       fts: FtsClient[F],
       analyser: TextAnalyser[F],
@@ -49,7 +50,7 @@ object ProcessItem {
   )(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
     processAttachments0[F](cfg, fts, analyser, regexNer, store, (30, 60, 90))(item)
 
-  def analysisOnly[F[_]: Async](
+  def analysisOnly[F[_]: Async: Files](
       cfg: Config,
       analyser: TextAnalyser[F],
       regexNer: RegexNerFile[F],
@@ -61,7 +62,7 @@ object ProcessItem {
       .flatMap(CrossCheckProposals[F](store))
       .flatMap(SaveProposals[F](store))
 
-  private def processAttachments0[F[_]: Async](
+  private def processAttachments0[F[_]: Async: Files](
       cfg: Config,
       fts: FtsClient[F],
       analyser: TextAnalyser[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala
index 6890e37d..58ade825 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala
@@ -9,6 +9,7 @@ package docspell.joex.process
 import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.addons.AddonTriggerType
 import docspell.analysis.TextAnalyser
@@ -30,7 +31,7 @@ import docspell.store.records.RItem
 object ReProcessItem {
   type Args = ReProcessItemArgs
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: Config,
       fts: FtsClient[F],
       itemOps: OItem[F],
@@ -106,7 +107,7 @@ object ReProcessItem {
       )
     }
 
-  def processFiles[F[_]: Async](
+  def processFiles[F[_]: Async: Files](
       cfg: Config,
       fts: FtsClient[F],
       itemOps: OItem[F],
@@ -162,7 +163,7 @@ object ReProcessItem {
   def isLastRetry[F[_]]: Task[F, Args, Boolean] =
     Task(_.isLastRetry)
 
-  def safeProcess[F[_]: Async](
+  def safeProcess[F[_]: Async: Files](
       cfg: Config,
       fts: FtsClient[F],
       itemOps: OItem[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/process/RunAddons.scala b/modules/joex/src/main/scala/docspell/joex/process/RunAddons.scala
index b564d8b4..5af296f6 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/RunAddons.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/RunAddons.scala
@@ -8,6 +8,7 @@ package docspell.joex.process
 
 import cats.effect._
 import cats.syntax.all._
+import fs2.io.file.Files
 
 import docspell.addons.AddonTriggerType
 import docspell.backend.joex.AddonOps
@@ -22,7 +23,7 @@ import docspell.store.Store
 object RunAddons {
   type Args = ProcessItemArgs
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       ops: AddonOps[F],
       store: Store[F],
       trigger: AddonTriggerType
diff --git a/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala b/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala
index 006a80ef..ee268949 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala
@@ -9,6 +9,7 @@ package docspell.joex.process
 import cats.Traverse
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.classifier.TextClassifier
 import docspell.analysis.{NlpSettings, TextAnalyser}
@@ -26,7 +27,7 @@ import docspell.store.records.{RAttachmentMeta, RClassifierSetting}
 object TextAnalysis {
   type Args = ProcessItemArgs
 
-  def apply[F[_]: Async](
+  def apply[F[_]: Async: Files](
       cfg: Config.TextAnalysis,
       analyser: TextAnalyser[F],
       nerFile: RegexNerFile[F],
@@ -87,7 +88,7 @@ object TextAnalysis {
     } yield (rm.copy(nerlabels = labels.all.toList), AttachmentDates(rm, labels.dates))
   }
 
-  def predictTags[F[_]: Async](
+  def predictTags[F[_]: Async: Files](
       ctx: Context[F, Args],
       store: Store[F],
       cfg: Config.TextAnalysis,
@@ -107,7 +108,7 @@ object TextAnalysis {
     } yield tags.flatten
   }
 
-  def predictItemEntities[F[_]: Async](
+  def predictItemEntities[F[_]: Async: Files](
       ctx: Context[F, Args],
       store: Store[F],
       cfg: Config.TextAnalysis,
@@ -139,7 +140,7 @@ object TextAnalysis {
       .map(MetaProposalList.apply)
   }
 
-  private def makeClassify[F[_]: Async](
+  private def makeClassify[F[_]: Async: Files](
       ctx: Context[F, Args],
       store: Store[F],
       cfg: Config.TextAnalysis,
diff --git a/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala b/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala
index 420e1d38..df00a3e2 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala
@@ -9,6 +9,7 @@ package docspell.joex.process
 import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.common._
 import docspell.extract.{ExtractConfig, ExtractResult, Extraction}
@@ -19,7 +20,7 @@ import docspell.store.records.{RAttachment, RAttachmentMeta, RFileMeta}
 
 object TextExtraction {
 
-  def apply[F[_]: Async](cfg: ExtractConfig, fts: FtsClient[F], store: Store[F])(
+  def apply[F[_]: Async: Files](cfg: ExtractConfig, fts: FtsClient[F], store: Store[F])(
       item: ItemData
   ): Task[F, ProcessItemArgs, ItemData] =
     Task { ctx =>
@@ -66,7 +67,7 @@ object TextExtraction {
 
   case class Result(am: RAttachmentMeta, td: TextData, tags: List[String] = Nil)
 
-  def extractTextIfEmpty[F[_]: Async](
+  def extractTextIfEmpty[F[_]: Async: Files](
       ctx: Context[F, ProcessItemArgs],
       store: Store[F],
       cfg: ExtractConfig,
@@ -100,7 +101,7 @@ object TextExtraction {
     }
   }
 
-  def extractTextToMeta[F[_]: Async](
+  def extractTextToMeta[F[_]: Async: Files](
       ctx: Context[F, _],
       store: Store[F],
       cfg: ExtractConfig,
@@ -143,7 +144,7 @@ object TextExtraction {
       .flatMap(mt => extr.extractText(data, DataType(mt), lang))
   }
 
-  private def extractTextFallback[F[_]: Async](
+  private def extractTextFallback[F[_]: Async: Files](
       ctx: Context[F, _],
       store: Store[F],
       cfg: ExtractConfig,
diff --git a/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala b/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala
index 1674235c..6ce9ed72 100644
--- a/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala
+++ b/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala
@@ -8,6 +8,7 @@ package docspell.joexapi.client
 
 import cats.effect._
 import cats.implicits._
+import fs2.io.net.Network
 
 import docspell.common.{Ident, LenientUri}
 import docspell.joexapi.model.{AddonSupport, BasicResult}
@@ -72,6 +73,6 @@ object JoexClient {
         Uri.unsafeFromString(u.asString)
     }
 
-  def resource[F[_]: Async]: Resource[F, JoexClient[F]] =
+  def resource[F[_]: Async: Network]: Resource[F, JoexClient[F]] =
     EmberClientBuilder.default[F].build.map(apply[F])
 }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala b/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala
index 58d3b9ca..e7132fd6 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/ConfigFile.scala
@@ -10,6 +10,7 @@ import java.security.SecureRandom
 
 import cats.Monoid
 import cats.effect.Async
+import fs2.io.file.Files
 
 import docspell.backend.auth.Login
 import docspell.backend.signup.{Config => SignupConfig}
@@ -30,7 +31,7 @@ object ConfigFile {
   // IntelliJ is wrong, this is required
   import Implicits._
 
-  def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
+  def loadConfig[F[_]: Async: Files](args: List[String]): F[Config] = {
     val logger = docspell.logging.getLogger[F]
     val validate =
       Validation.of(
diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala
index 513dceb4..d09be4fb 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala
@@ -9,6 +9,7 @@ package docspell.restserver
 import cats.effect._
 import fs2.Stream
 import fs2.concurrent.Topic
+import fs2.io.file.Files
 
 import docspell.backend.BackendApp
 import docspell.backend.auth.{AuthToken, ShareToken}
@@ -36,7 +37,7 @@ import org.http4s.client.Client
 import org.http4s.server.Router
 import org.http4s.server.websocket.WebSocketBuilder2
 
-final class RestAppImpl[F[_]: Async](
+final class RestAppImpl[F[_]: Async: Files](
     val config: Config,
     val backend: BackendApp[F],
     httpClient: Client[F],
@@ -162,7 +163,7 @@ final class RestAppImpl[F[_]: Async](
 
 object RestAppImpl {
 
-  def create[F[_]: Async](
+  def create[F[_]: Async: Files](
       cfg: Config,
       pools: Pools,
       store: Store[F],
diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
index 3b3a8755..400d5d27 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
@@ -12,6 +12,8 @@ import cats.effect._
 import cats.implicits._
 import fs2.Stream
 import fs2.concurrent.Topic
+import fs2.io.file.Files
+import fs2.io.net.Network
 
 import docspell.backend.msg.Topics
 import docspell.backend.ops.ONode
@@ -35,7 +37,7 @@ import org.http4s.server.websocket.WebSocketBuilder2
 
 object RestServer {
 
-  def serve[F[_]: Async](
+  def serve[F[_]: Async: Files: Network](
       cfg: Config,
       pools: Pools
   ): F[ExitCode] =
@@ -55,7 +57,7 @@ object RestServer {
           .flatMap { case (restApp, pubSub, setting) =>
             Stream(
               restApp.subscriptions,
-              restApp.eventConsume(2),
+              restApp.eventConsume(maxConcurrent = 2),
               Stream.resource {
                 if (cfg.serverOptions.enableHttp2)
                   EmberServerBuilder
@@ -81,7 +83,7 @@ object RestServer {
         (server ++ Stream(keepAlive)).parJoinUnbounded.compile.drain.as(ExitCode.Success)
     } yield exit
 
-  def createApp[F[_]: Async](
+  def createApp[F[_]: Async: Files: Network](
       cfg: Config,
       pools: Pools,
       wsTopic: Topic[F, OutputEvent]
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala
index 20bd3484..492e0ee5 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala
@@ -9,6 +9,7 @@ package docspell.restserver.routes
 import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.backend.BackendApp
 import docspell.backend.auth.AuthToken
@@ -25,7 +26,7 @@ import org.log4s._
 object UploadRoutes {
   private[this] val logger = getLogger
 
-  def secured[F[_]: Async](
+  def secured[F[_]: Async: Files](
       backend: BackendApp[F],
       cfg: Config,
       user: AuthToken
@@ -50,7 +51,7 @@ object UploadRoutes {
     }
   }
 
-  def open[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
+  def open[F[_]: Async: Files](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
@@ -78,7 +79,7 @@ object UploadRoutes {
     }
   }
 
-  private def submitFiles[F[_]: Async](
+  private def submitFiles[F[_]: Async: Files](
       backend: BackendApp[F],
       cfg: Config,
       accOrSrc: Either[Ident, CollectiveId],
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index daed5e41..6819c8db 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -20,9 +20,9 @@ object Dependencies {
   val EmilVersion = "0.13.0"
   val FlexmarkVersion = "0.64.8"
   val FlywayVersion = "9.22.3"
-  val Fs2Version = "3.6.1"
+  val Fs2Version = "3.9.2"
   val H2Version = "2.2.224"
-  val Http4sVersion = "0.23.18"
+  val Http4sVersion = "0.23.23"
   val Icu4jVersion = "74.1"
   val JavaOtpVersion = "0.4.0"
   val JsoupVersion = "1.16.2"