Refactoring for migrating to binny library

This commit is contained in:
eikek
2021-09-22 00:28:47 +02:00
parent 1f98d948b0
commit 20a829cf7a
45 changed files with 485 additions and 344 deletions

View File

@ -0,0 +1,29 @@
ALTER TABLE "filemeta" DROP COLUMN IF EXISTS "chunksize";
ALTER TABLE "filemeta" DROP COLUMN IF EXISTS "chunks";
ALTER TABLE "filemeta"
RENAME COLUMN "id" TO "file_id";
ALTER TABLE "filechunk"
RENAME COLUMN "fileid" TO "file_id";
ALTER TABLE "filechunk"
RENAME COLUMN "chunknr" TO "chunk_nr";
ALTER TABLE "filechunk"
RENAME COLUMN "chunklength" TO "chunk_len";
ALTER TABLE "filechunk"
RENAME COLUMN "chunkdata" TO "chunk_data";
-- change timestamp format, bitpeace used a string
ALTER TABLE "filemeta"
ADD COLUMN "created" timestamp;
UPDATE "filemeta" SET "created" = TO_TIMESTAMP("timestamp", 'YYYY-MM-DD"T"HH24:MI:SS.MS');
ALTER TABLE "filemeta"
ALTER COLUMN "created" SET NOT NULL;
ALTER TABLE "filemeta"
DROP COLUMN "timestamp";

View File

@ -0,0 +1,29 @@
ALTER TABLE `filemeta` DROP COLUMN IF EXISTS `chunksize`;
ALTER TABLE `filemeta` DROP COLUMN IF EXISTS `chunks`;
ALTER TABLE `filemeta`
RENAME COLUMN `id` TO `file_id`;
ALTER TABLE `filechunk`
RENAME COLUMN `fileid` TO `file_id`;
ALTER TABLE `filechunk`
RENAME COLUMN `chunknr` TO `chunk_nr`;
ALTER TABLE `filechunk`
RENAME COLUMN `chunklength` TO `chunk_len`;
ALTER TABLE `filechunk`
RENAME COLUMN `chunkdata` TO `chunk_data`;
-- change timestamp format, bitpeace used a string
ALTER TABLE `filemeta`
ADD COLUMN `created` timestamp;
UPDATE `filemeta` SET `created` = STR_TO_DATE(`timestamp`, '%Y-%m-%dT%H:%i:%s.%fZ');
ALTER TABLE `filemeta`
MODIFY `created` timestamp NOT NULL;
ALTER TABLE `filemeta`
DROP COLUMN `timestamp`;

View File

@ -0,0 +1,29 @@
ALTER TABLE "filemeta" DROP COLUMN IF EXISTS "chunksize";
ALTER TABLE "filemeta" DROP COLUMN IF EXISTS "chunks";
ALTER TABLE "filemeta"
RENAME COLUMN "id" TO "file_id";
ALTER TABLE "filechunk"
RENAME COLUMN "fileid" TO "file_id";
ALTER TABLE "filechunk"
RENAME COLUMN "chunknr" TO "chunk_nr";
ALTER TABLE "filechunk"
RENAME COLUMN "chunklength" TO "chunk_len";
ALTER TABLE "filechunk"
RENAME COLUMN "chunkdata" TO "chunk_data";
-- change timestamp format, bitpeace used a string
ALTER TABLE "filemeta"
ADD COLUMN "created" timestamp;
UPDATE "filemeta" SET "created" = CAST("timestamp" as timestamp);
ALTER TABLE "filemeta"
ALTER COLUMN "created" SET NOT NULL;
ALTER TABLE "filemeta"
DROP COLUMN "timestamp";

View File

@ -11,9 +11,10 @@ import scala.concurrent.ExecutionContext
import cats.effect._
import fs2._
import docspell.store.file.FileStore
import docspell.store.impl.StoreImpl
import bitpeace.Bitpeace
import com.zaxxer.hikari.HikariDataSource
import doobie._
import doobie.hikari.HikariTransactor
@ -23,7 +24,7 @@ trait Store[F[_]] {
def transact[A](prg: Stream[ConnectionIO, A]): Stream[F, A]
def bitpeace: Bitpeace[F]
def fileStore: FileStore[F]
def add(insert: ConnectionIO[Int], exists: ConnectionIO[Boolean]): F[AddResult]
}
@ -32,20 +33,23 @@ object Store {
def create[F[_]: Async](
jdbc: JdbcConfig,
chunkSize: Int,
connectEC: ExecutionContext
): Resource[F, Store[F]] = {
val hxa = HikariTransactor.newHikariTransactor[F](
jdbc.driverClass,
jdbc.url.asString,
jdbc.user,
jdbc.password,
connectEC
)
val acquire = Sync[F].delay(new HikariDataSource())
val free: HikariDataSource => F[Unit] = ds => Sync[F].delay(ds.close())
for {
xa <- hxa
st = new StoreImpl[F](jdbc, xa)
ds <- Resource.make(acquire)(free)
_ = Resource.pure {
ds.setJdbcUrl(jdbc.url.asString)
ds.setUsername(jdbc.user)
ds.setPassword(jdbc.password)
ds.setDriverClassName(jdbc.driverClass)
}
xa = HikariTransactor(ds, connectEC)
fs = FileStore[F](xa, ds, chunkSize)
st = new StoreImpl[F](fs, jdbc, xa)
_ <- Resource.eval(st.migrate)
} yield st
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.file
import cats.data.OptionT
import cats.effect._
import cats.implicits._
import docspell.common._
import docspell.store.records.RFileMeta
import binny._
import doobie._
import doobie.implicits._
final private[file] class AttributeStore[F[_]: Sync](xa: Transactor[F])
extends BinaryAttributeStore[F] {
def saveAttr(id: BinaryId, attrs: F[BinaryAttributes]): F[Unit] =
for {
now <- Timestamp.current[F]
a <- attrs
fm = RFileMeta(
Ident.unsafe(id.id),
now,
MimeType.parse(a.contentType.contentType).getOrElse(MimeType.octetStream),
ByteSize(a.length),
a.sha256
)
_ <- RFileMeta.insert(fm).transact(xa)
} yield ()
def deleteAttr(id: BinaryId): F[Boolean] =
RFileMeta.delete(Ident.unsafe(id.id)).transact(xa).map(_ > 0)
def findAttr(id: BinaryId): OptionT[F, BinaryAttributes] =
findMeta(id).map(fm =>
BinaryAttributes(
fm.checksum,
SimpleContentType(fm.mimetype.asString),
fm.length.bytes
)
)
def findMeta(id: BinaryId): OptionT[F, RFileMeta] =
OptionT(RFileMeta.findById(Ident.unsafe(id.id)).transact(xa))
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.file
import javax.sql.DataSource
import cats.data.OptionT
import cats.effect._
import fs2.{Pipe, Stream}
import docspell.common._
import docspell.store.records.RFileMeta
import binny._
import binny.jdbc.{GenericJdbcStore, JdbcStoreConfig}
import binny.tika.TikaContentTypeDetect
import doobie._
trait FileStore[F[_]] {
def find(id: Ident): OptionT[F, Stream[F, Byte]]
def getBytes(id: Ident): Stream[F, Byte]
def findMeta(id: Ident): OptionT[F, RFileMeta]
def delete(id: Ident): F[Unit]
def save(hint: MimeTypeHint): Pipe[F, Byte, Ident]
}
object FileStore {
private[this] val logger = org.log4s.getLogger
def apply[F[_]: Sync](
xa: Transactor[F],
ds: DataSource,
chunkSize: Int
): FileStore[F] = {
val attrStore = new AttributeStore[F](xa)
val cfg = JdbcStoreConfig("filechunk", chunkSize, TikaContentTypeDetect.default)
val binStore = GenericJdbcStore[F](ds, Log4sLogger[F](logger), cfg, attrStore)
new Impl[F](binStore, attrStore)
}
final private class Impl[F[_]](bs: BinaryStore[F], attrStore: AttributeStore[F])
extends FileStore[F] {
def find(id: Ident): OptionT[F, Stream[F, Byte]] =
bs.findBinary(BinaryId(id.id), ByteRange.All)
def getBytes(id: Ident): Stream[F, Byte] =
Stream.eval(find(id).value).unNoneTerminate.flatMap(identity)
def findMeta(id: Ident): OptionT[F, RFileMeta] =
attrStore.findMeta(BinaryId(id.id))
def delete(id: Ident): F[Unit] =
bs.delete(BinaryId(id.id))
def save(hint: MimeTypeHint): Pipe[F, Byte, Ident] =
bs.insert(Hint(hint.filename, hint.advertised))
.andThen(_.map(bid => Ident.unsafe(bid.id)))
}
private object Log4sLogger {
def apply[F[_]: Sync](log: org.log4s.Logger): binny.util.Logger[F] =
new binny.util.Logger[F] {
override def trace(msg: => String): F[Unit] =
Sync[F].delay(log.trace(msg))
override def debug(msg: => String): F[Unit] =
Sync[F].delay(log.debug(msg))
override def info(msg: => String): F[Unit] =
Sync[F].delay(log.info(msg))
override def warn(msg: => String): F[Unit] =
Sync[F].delay(log.warn(msg))
override def error(msg: => String): F[Unit] =
Sync[F].delay(log.error(msg))
override def error(ex: Throwable)(msg: => String): F[Unit] =
Sync[F].delay(log.error(ex)(msg))
}
}
}

View File

@ -20,6 +20,7 @@ import doobie.util.log.Success
import emil.doobie.EmilDoobieMeta
import io.circe.Json
import io.circe.{Decoder, Encoder}
import scodec.bits.ByteVector
trait DoobieMeta extends EmilDoobieMeta {
@ -132,6 +133,15 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaKey: Meta[Key] =
Meta[String].timap(Key.unsafeFromString)(_.asString)
implicit val metaMimeType: Meta[MimeType] =
Meta[String].timap(MimeType.unsafe)(_.asString)
implicit val metaByteVectorHex: Meta[ByteVector] =
Meta[String].timap(s => ByteVector.fromValidHex(s))(_.toHex)
implicit val metaByteSize: Meta[ByteSize] =
Meta[Long].timap(ByteSize.apply)(_.bytes)
}
object DoobieMeta extends DoobieMeta {

View File

@ -9,22 +9,18 @@ package docspell.store.impl
import cats.effect.Async
import cats.implicits._
import docspell.common.Ident
import docspell.store.file.FileStore
import docspell.store.migrate.FlywayMigrate
import docspell.store.{AddResult, JdbcConfig, Store}
import bitpeace.{Bitpeace, BitpeaceConfig, TikaMimetypeDetect}
import doobie._
import doobie.implicits._
final class StoreImpl[F[_]: Async](jdbc: JdbcConfig, xa: Transactor[F]) extends Store[F] {
val bitpeaceCfg =
BitpeaceConfig(
"filemeta",
"filechunk",
TikaMimetypeDetect,
Ident.randomId[F].map(_.id)
)
final class StoreImpl[F[_]: Async](
val fileStore: FileStore[F],
jdbc: JdbcConfig,
xa: Transactor[F]
) extends Store[F] {
def migrate: F[Int] =
FlywayMigrate.run[F](jdbc).map(_.migrationsExecuted)
@ -35,9 +31,6 @@ final class StoreImpl[F[_]: Async](jdbc: JdbcConfig, xa: Transactor[F]) extends
def transact[A](prg: fs2.Stream[doobie.ConnectionIO, A]): fs2.Stream[F, A] =
prg.transact(xa)
def bitpeace: Bitpeace[F] =
Bitpeace(bitpeaceCfg, xa)
def add(insert: ConnectionIO[Int], exists: ConnectionIO[Boolean]): F[AddResult] =
for {
save <- transact(insert).attempt

View File

@ -9,8 +9,6 @@ package docspell.store.queries
import docspell.common._
import docspell.store.records._
import bitpeace.FileMeta
case class ItemData(
item: RItem,
corrOrg: Option[ROrganization],
@ -20,9 +18,9 @@ case class ItemData(
inReplyTo: Option[IdRef],
folder: Option[IdRef],
tags: Vector[RTag],
attachments: Vector[(RAttachment, FileMeta)],
sources: Vector[(RAttachmentSource, FileMeta)],
archives: Vector[(RAttachmentArchive, FileMeta)],
attachments: Vector[(RAttachment, RFileMeta)],
sources: Vector[(RAttachmentSource, RFileMeta)],
archives: Vector[(RAttachmentArchive, RFileMeta)],
customFields: Vector[ItemFieldValue]
) {

View File

@ -38,10 +38,10 @@ object QAttachment {
Stream
.evalSeq(store.transact(findPreview))
.map(_.fileId.id)
.map(_.fileId)
.evalTap(_ => store.transact(RAttachmentPreview.delete(attachId)))
.flatMap(store.bitpeace.delete)
.map(flag => if (flag) 1 else 0)
.evalMap(store.fileStore.delete)
.map(_ => 1)
.compile
.foldMonoid
}
@ -68,9 +68,8 @@ object QAttachment {
f <-
Stream
.emits(files._1)
.map(_.id)
.flatMap(store.bitpeace.delete)
.map(flag => if (flag) 1 else 0)
.evalMap(store.fileStore.delete)
.map(_ => 1)
.compile
.foldMonoid
} yield n + k + f
@ -91,9 +90,9 @@ object QAttachment {
)
f <-
Stream
.emits(ra.fileId.id +: (s.map(_.fileId.id).toSeq ++ p.map(_.fileId.id).toSeq))
.flatMap(store.bitpeace.delete)
.map(flag => if (flag) 1 else 0)
.emits(ra.fileId +: (s.map(_.fileId).toSeq ++ p.map(_.fileId).toSeq))
.evalMap(store.fileStore.delete)
.map(_ => 1)
.compile
.foldMonoid
} yield n + f
@ -104,8 +103,8 @@ object QAttachment {
n <- OptionT.liftF(store.transact(RAttachmentArchive.deleteAll(aa.fileId)))
_ <- OptionT.liftF(
Stream
.emit(aa.fileId.id)
.flatMap(store.bitpeace.delete)
.emit(aa.fileId)
.evalMap(store.fileStore.delete)
.compile
.drain
)

View File

@ -99,16 +99,16 @@ object QCollective {
inner join item i on a.itemid = i.itemid
where i.cid = $coll)
select a.fid,m.length from attachs a
inner join filemeta m on m.id = a.fid
inner join filemeta m on m.file_id = a.fid
union distinct
select a.file_id,m.length from attachment_source a
inner join filemeta m on m.id = a.file_id where a.id in (select aid from attachs)
inner join filemeta m on m.file_id = a.file_id where a.id in (select aid from attachs)
union distinct
select p.file_id,m.length from attachment_preview p
inner join filemeta m on m.id = p.file_id where p.id in (select aid from attachs)
inner join filemeta m on m.file_id = p.file_id where p.id in (select aid from attachs)
union distinct
select a.file_id,m.length from attachment_archive a
inner join filemeta m on m.id = a.file_id where a.id in (select aid from attachs)
inner join filemeta m on m.file_id = a.file_id where a.id in (select aid from attachs)
) as t""".query[Option[Long]].unique
for {

View File

@ -496,7 +496,7 @@ object QItem {
where(
i.cid === collective &&
i.state.in(ItemState.validStates) &&
Condition.Or(fms.map(m => m.checksum === checksum)) &&?
Condition.Or(fms.map(m => m.checksum ==== checksum)) &&?
Nel
.fromList(excludeFileMeta.toList)
.map(excl => Condition.And(fms.map(m => m.id.isNull || m.id.notIn(excl))))

View File

@ -14,7 +14,6 @@ import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import bitpeace.FileMeta
import doobie._
import doobie.implicits._
@ -113,9 +112,7 @@ object RAttachment {
def findById(attachId: Ident): ConnectionIO[Option[RAttachment]] =
run(select(T.all), from(T), T.id === attachId).query[RAttachment].option
def findMeta(attachId: Ident): ConnectionIO[Option[FileMeta]] = {
import bitpeace.sql._
def findMeta(attachId: Ident): ConnectionIO[Option[RFileMeta]] = {
val m = RFileMeta.as("m")
val a = RAttachment.as("a")
Select(
@ -123,7 +120,7 @@ object RAttachment {
from(a)
.innerJoin(m, a.fileId === m.id),
a.id === attachId
).build.query[FileMeta].option
).build.query[RFileMeta].option
}
def updateName(
@ -206,9 +203,7 @@ object RAttachment {
def findByItemAndCollectiveWithMeta(
id: Ident,
coll: Ident
): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
import bitpeace.sql._
): ConnectionIO[Vector[(RAttachment, RFileMeta)]] = {
val a = RAttachment.as("a")
val m = RFileMeta.as("m")
val i = RItem.as("i")
@ -218,12 +213,10 @@ object RAttachment {
.innerJoin(m, a.fileId === m.id)
.innerJoin(i, a.itemId === i.id),
a.itemId === id && i.cid === coll
).build.query[(RAttachment, FileMeta)].to[Vector]
).build.query[(RAttachment, RFileMeta)].to[Vector]
}
def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
import bitpeace.sql._
def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, RFileMeta)]] = {
val a = RAttachment.as("a")
val m = RFileMeta.as("m")
Select(
@ -231,7 +224,7 @@ object RAttachment {
from(a)
.innerJoin(m, a.fileId === m.id),
a.itemId === id
).orderBy(a.position.asc).build.query[(RAttachment, FileMeta)].to[Vector]
).orderBy(a.position.asc).build.query[(RAttachment, RFileMeta)].to[Vector]
}
/** Deletes the attachment and its related source and meta records.

View File

@ -13,7 +13,6 @@ import docspell.store.qb.DSL._
import docspell.store.qb.TableDef
import docspell.store.qb._
import bitpeace.FileMeta
import doobie._
import doobie.implicits._
@ -98,9 +97,7 @@ object RAttachmentArchive {
def findByItemWithMeta(
id: Ident
): ConnectionIO[Vector[(RAttachmentArchive, FileMeta)]] = {
import bitpeace.sql._
): ConnectionIO[Vector[(RAttachmentArchive, RFileMeta)]] = {
val a = RAttachmentArchive.as("a")
val b = RAttachment.as("b")
val m = RFileMeta.as("m")
@ -110,7 +107,7 @@ object RAttachmentArchive {
.innerJoin(m, a.fileId === m.id)
.innerJoin(b, a.id === b.id),
b.itemId === id
).orderBy(b.position.asc).build.query[(RAttachmentArchive, FileMeta)].to[Vector]
).orderBy(b.position.asc).build.query[(RAttachmentArchive, RFileMeta)].to[Vector]
}
/** If the given attachment id has an associated archive, this returns the number of all

View File

@ -12,7 +12,6 @@ import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import bitpeace.FileMeta
import doobie._
import doobie.implicits._
@ -101,9 +100,7 @@ object RAttachmentPreview {
def findByItemWithMeta(
id: Ident
): ConnectionIO[Vector[(RAttachmentPreview, FileMeta)]] = {
import bitpeace.sql._
): ConnectionIO[Vector[(RAttachmentPreview, RFileMeta)]] = {
val a = RAttachmentPreview.as("a")
val b = RAttachment.as("b")
val m = RFileMeta.as("m")
@ -114,6 +111,6 @@ object RAttachmentPreview {
.innerJoin(m, a.fileId === m.id)
.innerJoin(b, b.id === a.id),
b.itemId === id
).orderBy(b.position.asc).build.query[(RAttachmentPreview, FileMeta)].to[Vector]
).orderBy(b.position.asc).build.query[(RAttachmentPreview, RFileMeta)].to[Vector]
}
}

View File

@ -12,7 +12,6 @@ import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import bitpeace.FileMeta
import doobie._
import doobie.implicits._
@ -97,9 +96,7 @@ object RAttachmentSource {
def findByItemWithMeta(
id: Ident
): ConnectionIO[Vector[(RAttachmentSource, FileMeta)]] = {
import bitpeace.sql._
): ConnectionIO[Vector[(RAttachmentSource, RFileMeta)]] = {
val a = RAttachmentSource.as("a")
val b = RAttachment.as("b")
val m = RFileMeta.as("m")
@ -110,7 +107,7 @@ object RAttachmentSource {
.innerJoin(m, a.fileId === m.id)
.innerJoin(b, b.id === a.id),
b.itemId === id
).orderBy(b.position.asc).build.query[(RAttachmentSource, FileMeta)].to[Vector]
).orderBy(b.position.asc).build.query[(RAttachmentSource, RFileMeta)].to[Vector]
}
}

View File

@ -6,35 +6,37 @@
package docspell.store.records
import java.time.Instant
import cats.data.NonEmptyList
import cats.implicits._
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import docspell.store.syntax.MimeTypes._
import bitpeace.FileMeta
import bitpeace.Mimetype
import doobie._
import doobie.implicits._
import scodec.bits.ByteVector
final case class RFileMeta(
id: Ident,
created: Timestamp,
mimetype: MimeType,
length: ByteSize,
checksum: ByteVector
)
object RFileMeta {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "filemeta"
val id = Column[Ident]("id", this)
val timestamp = Column[Instant]("timestamp", this)
val mimetype = Column[Mimetype]("mimetype", this)
val length = Column[Long]("length", this)
val checksum = Column[String]("checksum", this)
val chunks = Column[Int]("chunks", this)
val chunksize = Column[Int]("chunksize", this)
val id = Column[Ident]("file_id", this)
val timestamp = Column[Timestamp]("created", this)
val mimetype = Column[MimeType]("mimetype", this)
val length = Column[ByteSize]("length", this)
val checksum = Column[ByteVector]("checksum", this)
val all = NonEmptyList
.of[Column[_]](id, timestamp, mimetype, length, checksum, chunks, chunksize)
.of[Column[_]](id, timestamp, mimetype, length, checksum)
}
@ -42,29 +44,25 @@ object RFileMeta {
def as(alias: String): Table =
Table(Some(alias))
def findById(fid: Ident): ConnectionIO[Option[FileMeta]] = {
import bitpeace.sql._
def insert(r: RFileMeta): ConnectionIO[Int] =
DML.insert(T, T.all, fr"${r.id},${r.created},${r.mimetype},${r.length},${r.checksum}")
run(select(T.all), from(T), T.id === fid).query[FileMeta].option
}
def findByIds(ids: List[Ident]): ConnectionIO[Vector[FileMeta]] = {
import bitpeace.sql._
def findById(fid: Ident): ConnectionIO[Option[RFileMeta]] =
run(select(T.all), from(T), T.id === fid).query[RFileMeta].option
def findByIds(ids: List[Ident]): ConnectionIO[Vector[RFileMeta]] =
NonEmptyList.fromList(ids) match {
case Some(nel) =>
run(select(T.all), from(T), T.id.in(nel)).query[FileMeta].to[Vector]
run(select(T.all), from(T), T.id.in(nel)).query[RFileMeta].to[Vector]
case None =>
Vector.empty[FileMeta].pure[ConnectionIO]
Vector.empty[RFileMeta].pure[ConnectionIO]
}
}
def findMime(fid: Ident): ConnectionIO[Option[MimeType]] = {
import bitpeace.sql._
def findMime(fid: Ident): ConnectionIO[Option[MimeType]] =
run(select(T.mimetype), from(T), T.id === fid)
.query[Mimetype]
.query[MimeType]
.option
.map(_.map(_.toLocal))
}
def delete(id: Ident): ConnectionIO[Int] =
DML.delete(T, T.id === id)
}

View File

@ -8,16 +8,8 @@ package docspell.store.syntax
import docspell.common._
import bitpeace.Mimetype
object MimeTypes {
implicit final class BitpeaceMimeTypeOps(bmt: Mimetype) {
def toLocal: MimeType =
MimeType(bmt.primary, bmt.sub, bmt.params)
}
implicit final class EmilMimeTypeOps(emt: emil.MimeType) {
def toLocal: MimeType =
MimeType(emt.primary, emt.sub, emt.params)
@ -26,8 +18,5 @@ object MimeTypes {
implicit final class DocspellMimeTypeOps(mt: MimeType) {
def toEmil: emil.MimeType =
emil.MimeType(mt.primary, mt.sub, mt.params)
def toBitpeace: Mimetype =
Mimetype(mt.primary, mt.sub, mt.params)
}
}