mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 10:28:27 +00:00
Add a file-repository for better organizing files
Docspell now must use a new api for accessing files. Issue: #1379
This commit is contained in:
@ -12,7 +12,7 @@ import cats.effect._
|
||||
import cats.~>
|
||||
import fs2._
|
||||
|
||||
import docspell.store.file.FileStore
|
||||
import docspell.store.file.FileRepository
|
||||
import docspell.store.impl.StoreImpl
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
@ -26,7 +26,7 @@ trait Store[F[_]] {
|
||||
|
||||
def transact[A](prg: Stream[ConnectionIO, A]): Stream[F, A]
|
||||
|
||||
def fileStore: FileStore[F]
|
||||
def fileRepo: FileRepository[F]
|
||||
|
||||
def add(insert: ConnectionIO[Int], exists: ConnectionIO[Boolean]): F[AddResult]
|
||||
}
|
||||
@ -50,8 +50,8 @@ object Store {
|
||||
ds.setDriverClassName(jdbc.driverClass)
|
||||
}
|
||||
xa = HikariTransactor(ds, connectEC)
|
||||
fs = FileStore[F](xa, ds, chunkSize)
|
||||
st = new StoreImpl[F](fs, jdbc, xa)
|
||||
fr = FileRepository.genericJDBC(xa, ds, chunkSize)
|
||||
st = new StoreImpl[F](fr, jdbc, xa)
|
||||
_ <- Resource.eval(st.migrate)
|
||||
} yield st
|
||||
}
|
||||
|
@ -23,8 +23,9 @@ final private[file] class AttributeStore[F[_]: Sync](xa: Transactor[F])
|
||||
for {
|
||||
now <- Timestamp.current[F]
|
||||
a <- attrs
|
||||
fileKey <- makeFileKey(id)
|
||||
fm = RFileMeta(
|
||||
Ident.unsafe(id.id),
|
||||
fileKey,
|
||||
now,
|
||||
MimeType.parse(a.contentType.contentType).getOrElse(MimeType.octetStream),
|
||||
ByteSize(a.length),
|
||||
@ -34,7 +35,7 @@ final private[file] class AttributeStore[F[_]: Sync](xa: Transactor[F])
|
||||
} yield ()
|
||||
|
||||
def deleteAttr(id: BinaryId): F[Boolean] =
|
||||
RFileMeta.delete(Ident.unsafe(id.id)).transact(xa).map(_ > 0)
|
||||
makeFileKey(id).flatMap(fileKey => RFileMeta.delete(fileKey).transact(xa).map(_ > 0))
|
||||
|
||||
def findAttr(id: BinaryId): OptionT[F, BinaryAttributes] =
|
||||
findMeta(id).map(fm =>
|
||||
@ -46,5 +47,10 @@ final private[file] class AttributeStore[F[_]: Sync](xa: Transactor[F])
|
||||
)
|
||||
|
||||
def findMeta(id: BinaryId): OptionT[F, RFileMeta] =
|
||||
OptionT(RFileMeta.findById(Ident.unsafe(id.id)).transact(xa))
|
||||
OptionT(makeFileKey(id).flatMap(fileKey => RFileMeta.findById(fileKey).transact(xa)))
|
||||
|
||||
private def makeFileKey(binaryId: BinaryId): F[FileKey] =
|
||||
Sync[F]
|
||||
.pure(BinnyUtils.binaryIdToFileKey(binaryId).left.map(new IllegalStateException(_)))
|
||||
.rethrow
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.file
|
||||
|
||||
import docspell.common
|
||||
import docspell.common._
|
||||
import docspell.files.TikaMimetype
|
||||
|
||||
import binny._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
private[store] object BinnyUtils {
|
||||
|
||||
def fileKeyToBinaryId(fk: FileKey): BinaryId =
|
||||
BinaryId(s"${fk.collective.id}/${fk.category.id.id}/${fk.id.id}")
|
||||
|
||||
def binaryIdToFileKey(bid: BinaryId): Either[String, FileKey] =
|
||||
bid.id.split('/').toList match {
|
||||
case cId :: catId :: fId :: Nil =>
|
||||
for {
|
||||
coll <- Ident.fromString(cId)
|
||||
cat <- FileCategory.fromString(catId)
|
||||
file <- Ident.fromString(fId)
|
||||
} yield common.FileKey(coll, cat, file)
|
||||
case _ =>
|
||||
Left(s"Invalid format for file-key: $bid")
|
||||
}
|
||||
|
||||
def unsafeBinaryIdToFileKey(bid: BinaryId): FileKey =
|
||||
binaryIdToFileKey(bid).fold(
|
||||
err => throw new IllegalStateException(err),
|
||||
identity
|
||||
)
|
||||
|
||||
object LoggerAdapter {
|
||||
def apply[F[_]](log: Logger[F]): binny.util.Logger[F] =
|
||||
new binny.util.Logger[F] {
|
||||
override def trace(msg: => String): F[Unit] = log.trace(msg)
|
||||
override def debug(msg: => String): F[Unit] = log.debug(msg)
|
||||
override def info(msg: => String): F[Unit] = log.info(msg)
|
||||
override def warn(msg: => String): F[Unit] = log.warn(msg)
|
||||
override def error(msg: => String): F[Unit] = log.error(msg)
|
||||
override def error(ex: Throwable)(msg: => String): F[Unit] = log.error(ex)(msg)
|
||||
}
|
||||
}
|
||||
|
||||
object TikaContentTypeDetect extends ContentTypeDetect {
|
||||
override def detect(data: ByteVector, hint: Hint): SimpleContentType =
|
||||
SimpleContentType(
|
||||
TikaMimetype
|
||||
.detect(data, MimeTypeHint(hint.filename, hint.advertisedType))
|
||||
.asString
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.file
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
final case class FileMetadata(
|
||||
id: FileKey,
|
||||
created: Timestamp,
|
||||
mimetype: MimeType,
|
||||
length: ByteSize,
|
||||
checksum: ByteVector
|
||||
)
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.file
|
||||
|
||||
import javax.sql.DataSource
|
||||
|
||||
import cats.effect._
|
||||
import fs2._
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import binny.BinaryId
|
||||
import binny.jdbc.{GenericJdbcStore, JdbcStoreConfig}
|
||||
import doobie.Transactor
|
||||
|
||||
trait FileRepository[F[_]] {
|
||||
def getBytes(key: FileKey): Stream[F, Byte]
|
||||
|
||||
def findMeta(key: FileKey): F[Option[FileMetadata]]
|
||||
|
||||
def delete(key: FileKey): F[Unit]
|
||||
|
||||
def save(
|
||||
collective: Ident,
|
||||
category: FileCategory,
|
||||
hint: MimeTypeHint
|
||||
): Pipe[F, Byte, FileKey]
|
||||
}
|
||||
|
||||
object FileRepository {
|
||||
private[this] val logger = org.log4s.getLogger
|
||||
|
||||
def genericJDBC[F[_]: Sync](
|
||||
xa: Transactor[F],
|
||||
ds: DataSource,
|
||||
chunkSize: Int
|
||||
): FileRepository[F] = {
|
||||
val attrStore = new AttributeStore[F](xa)
|
||||
val cfg = JdbcStoreConfig("filechunk", chunkSize, BinnyUtils.TikaContentTypeDetect)
|
||||
val log = Logger.log4s[F](logger)
|
||||
val binStore = GenericJdbcStore[F](ds, BinnyUtils.LoggerAdapter(log), cfg, attrStore)
|
||||
val keyFun: FileKey => BinaryId = BinnyUtils.fileKeyToBinaryId
|
||||
|
||||
new FileRepositoryImpl[F](binStore, attrStore, keyFun)
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.file
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
import fs2.{Pipe, Stream}
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import binny._
|
||||
|
||||
final class FileRepositoryImpl[F[_]: Sync](
|
||||
bs: BinaryStore[F],
|
||||
attrStore: AttributeStore[F],
|
||||
keyFun: FileKey => BinaryId
|
||||
) extends FileRepository[F] {
|
||||
|
||||
def find(key: FileKey): OptionT[F, Stream[F, Byte]] =
|
||||
bs.findBinary(keyFun(key), ByteRange.All)
|
||||
|
||||
def getBytes(key: FileKey): Stream[F, Byte] =
|
||||
Stream.eval(find(key).value).unNoneTerminate.flatMap(identity)
|
||||
|
||||
def findMeta(key: FileKey): F[Option[FileMetadata]] =
|
||||
attrStore
|
||||
.findMeta(keyFun(key))
|
||||
.map(rfm =>
|
||||
FileMetadata(rfm.id, rfm.created, rfm.mimetype, rfm.length, rfm.checksum)
|
||||
)
|
||||
.value
|
||||
|
||||
def delete(key: FileKey): F[Unit] =
|
||||
bs.delete(keyFun(key))
|
||||
|
||||
def save(
|
||||
collective: Ident,
|
||||
category: FileCategory,
|
||||
hint: MimeTypeHint
|
||||
): Pipe[F, Byte, FileKey] = {
|
||||
val fhint = Hint(hint.filename, hint.advertised)
|
||||
in =>
|
||||
Stream
|
||||
.eval(randomKey(collective, category))
|
||||
.flatMap(fkey =>
|
||||
in.through(bs.insertWith(keyFun(fkey), fhint)) ++ Stream.emit(fkey)
|
||||
)
|
||||
}
|
||||
|
||||
def randomKey(
|
||||
collective: Ident,
|
||||
category: FileCategory
|
||||
): F[FileKey] =
|
||||
BinaryId.random[F].map(bid => FileKey(collective, category, Ident.unsafe(bid.id)))
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* 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.files.TikaMimetype
|
||||
import docspell.store.records.RFileMeta
|
||||
|
||||
import binny._
|
||||
import binny.jdbc.{GenericJdbcStore, JdbcStoreConfig}
|
||||
import doobie._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
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)
|
||||
val log = Logger.log4s[F](logger)
|
||||
val binStore = GenericJdbcStore[F](ds, LoggerAdapter(log), 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 LoggerAdapter {
|
||||
def apply[F[_]](log: Logger[F]): binny.util.Logger[F] =
|
||||
new binny.util.Logger[F] {
|
||||
override def trace(msg: => String): F[Unit] = log.trace(msg)
|
||||
override def debug(msg: => String): F[Unit] = log.debug(msg)
|
||||
override def info(msg: => String): F[Unit] = log.info(msg)
|
||||
override def warn(msg: => String): F[Unit] = log.warn(msg)
|
||||
override def error(msg: => String): F[Unit] = log.error(msg)
|
||||
override def error(ex: Throwable)(msg: => String): F[Unit] = log.error(ex)(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private object TikaContentTypeDetect extends ContentTypeDetect {
|
||||
override def detect(data: ByteVector, hint: Hint): SimpleContentType =
|
||||
SimpleContentType(
|
||||
TikaMimetype
|
||||
.detect(data, MimeTypeHint(hint.filename, hint.advertisedType))
|
||||
.asString
|
||||
)
|
||||
}
|
||||
}
|
@ -14,8 +14,10 @@ import docspell.common.syntax.all._
|
||||
import docspell.jsonminiq.JsonMiniQuery
|
||||
import docspell.notification.api.{ChannelType, EventType}
|
||||
import docspell.query.{ItemQuery, ItemQueryParser}
|
||||
import docspell.store.file.BinnyUtils
|
||||
import docspell.totp.Key
|
||||
|
||||
import binny.BinaryId
|
||||
import com.github.eikek.calev.CalEvent
|
||||
import doobie._
|
||||
import doobie.implicits.legacy.instant._
|
||||
@ -27,7 +29,7 @@ import scodec.bits.ByteVector
|
||||
|
||||
trait DoobieMeta extends EmilDoobieMeta {
|
||||
|
||||
implicit val sqlLogging = LogHandler {
|
||||
implicit val sqlLogging: LogHandler = LogHandler {
|
||||
case e @ Success(_, _, _, _) =>
|
||||
DoobieMeta.logger.trace("SQL " + e)
|
||||
case e =>
|
||||
@ -39,58 +41,64 @@ trait DoobieMeta extends EmilDoobieMeta {
|
||||
e.apply(a).noSpaces
|
||||
)
|
||||
|
||||
implicit val metaBinaryId: Meta[BinaryId] =
|
||||
Meta[String].timap(BinaryId.apply)(_.id)
|
||||
|
||||
implicit val metaFileKey: Meta[FileKey] =
|
||||
Meta[BinaryId].timap(BinnyUtils.unsafeBinaryIdToFileKey)(BinnyUtils.fileKeyToBinaryId)
|
||||
|
||||
implicit val metaAccountSource: Meta[AccountSource] =
|
||||
Meta[String].imap(AccountSource.unsafeFromString)(_.name)
|
||||
Meta[String].timap(AccountSource.unsafeFromString)(_.name)
|
||||
|
||||
implicit val metaDuration: Meta[Duration] =
|
||||
Meta[Long].imap(Duration.millis)(_.millis)
|
||||
Meta[Long].timap(Duration.millis)(_.millis)
|
||||
|
||||
implicit val metaCollectiveState: Meta[CollectiveState] =
|
||||
Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString)
|
||||
Meta[String].timap(CollectiveState.unsafe)(CollectiveState.asString)
|
||||
|
||||
implicit val metaUserState: Meta[UserState] =
|
||||
Meta[String].imap(UserState.unsafe)(UserState.asString)
|
||||
Meta[String].timap(UserState.unsafe)(UserState.asString)
|
||||
|
||||
implicit val metaPassword: Meta[Password] =
|
||||
Meta[String].imap(Password(_))(_.pass)
|
||||
Meta[String].timap(Password(_))(_.pass)
|
||||
|
||||
implicit val metaIdent: Meta[Ident] =
|
||||
Meta[String].imap(Ident.unsafe)(_.id)
|
||||
Meta[String].timap(Ident.unsafe)(_.id)
|
||||
|
||||
implicit val metaContactKind: Meta[ContactKind] =
|
||||
Meta[String].imap(ContactKind.unsafe)(_.asString)
|
||||
Meta[String].timap(ContactKind.unsafe)(_.asString)
|
||||
|
||||
implicit val metaTimestamp: Meta[Timestamp] =
|
||||
Meta[Instant].imap(Timestamp(_))(_.value)
|
||||
Meta[Instant].timap(Timestamp(_))(_.value)
|
||||
|
||||
implicit val metaJobState: Meta[JobState] =
|
||||
Meta[String].imap(JobState.unsafe)(_.name)
|
||||
Meta[String].timap(JobState.unsafe)(_.name)
|
||||
|
||||
implicit val metaDirection: Meta[Direction] =
|
||||
Meta[Boolean].imap(flag =>
|
||||
Meta[Boolean].timap(flag =>
|
||||
if (flag) Direction.Incoming: Direction else Direction.Outgoing: Direction
|
||||
)(d => Direction.isIncoming(d))
|
||||
|
||||
implicit val metaPriority: Meta[Priority] =
|
||||
Meta[Int].imap(Priority.fromInt)(Priority.toInt)
|
||||
Meta[Int].timap(Priority.fromInt)(Priority.toInt)
|
||||
|
||||
implicit val metaLogLevel: Meta[LogLevel] =
|
||||
Meta[String].imap(LogLevel.unsafeString)(_.name)
|
||||
Meta[String].timap(LogLevel.unsafeString)(_.name)
|
||||
|
||||
implicit val metaLenientUri: Meta[LenientUri] =
|
||||
Meta[String].imap(LenientUri.unsafe)(_.asString)
|
||||
Meta[String].timap(LenientUri.unsafe)(_.asString)
|
||||
|
||||
implicit val metaNodeType: Meta[NodeType] =
|
||||
Meta[String].imap(NodeType.unsafe)(_.name)
|
||||
Meta[String].timap(NodeType.unsafe)(_.name)
|
||||
|
||||
implicit val metaLocalDate: Meta[LocalDate] =
|
||||
Meta[String].imap(str => LocalDate.parse(str))(_.format(DateTimeFormatter.ISO_DATE))
|
||||
Meta[String].timap(str => LocalDate.parse(str))(_.format(DateTimeFormatter.ISO_DATE))
|
||||
|
||||
implicit val metaItemState: Meta[ItemState] =
|
||||
Meta[String].imap(ItemState.unsafe)(_.name)
|
||||
Meta[String].timap(ItemState.unsafe)(_.name)
|
||||
|
||||
implicit val metNerTag: Meta[NerTag] =
|
||||
Meta[String].imap(NerTag.unsafe)(_.name)
|
||||
Meta[String].timap(NerTag.unsafe)(_.name)
|
||||
|
||||
implicit val metaNerLabel: Meta[NerLabel] =
|
||||
jsonMeta[NerLabel]
|
||||
@ -108,7 +116,7 @@ trait DoobieMeta extends EmilDoobieMeta {
|
||||
jsonMeta[List[IdRef]]
|
||||
|
||||
implicit val metaLanguage: Meta[Language] =
|
||||
Meta[String].imap(Language.unsafe)(_.iso3)
|
||||
Meta[String].timap(Language.unsafe)(_.iso3)
|
||||
|
||||
implicit val metaCalEvent: Meta[CalEvent] =
|
||||
Meta[String].timap(CalEvent.unsafe)(_.asString)
|
||||
|
@ -11,7 +11,7 @@ import cats.effect.Async
|
||||
import cats.implicits._
|
||||
import cats.~>
|
||||
|
||||
import docspell.store.file.FileStore
|
||||
import docspell.store.file.FileRepository
|
||||
import docspell.store.migrate.FlywayMigrate
|
||||
import docspell.store.{AddResult, JdbcConfig, Store}
|
||||
|
||||
@ -19,7 +19,7 @@ import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
final class StoreImpl[F[_]: Async](
|
||||
val fileStore: FileStore[F],
|
||||
val fileRepo: FileRepository[F],
|
||||
jdbc: JdbcConfig,
|
||||
xa: Transactor[F]
|
||||
) extends Store[F] {
|
||||
@ -30,10 +30,10 @@ final class StoreImpl[F[_]: Async](
|
||||
def migrate: F[Int] =
|
||||
FlywayMigrate.run[F](jdbc).map(_.migrationsExecuted)
|
||||
|
||||
def transact[A](prg: doobie.ConnectionIO[A]): F[A] =
|
||||
def transact[A](prg: ConnectionIO[A]): F[A] =
|
||||
prg.transact(xa)
|
||||
|
||||
def transact[A](prg: fs2.Stream[doobie.ConnectionIO, A]): fs2.Stream[F, A] =
|
||||
def transact[A](prg: fs2.Stream[ConnectionIO, A]): fs2.Stream[F, A] =
|
||||
prg.transact(xa)
|
||||
|
||||
def add(insert: ConnectionIO[Int], exists: ConnectionIO[Boolean]): F[AddResult] =
|
||||
|
@ -40,7 +40,7 @@ object QAttachment {
|
||||
.evalSeq(store.transact(findPreview))
|
||||
.map(_.fileId)
|
||||
.evalTap(_ => store.transact(RAttachmentPreview.delete(attachId)))
|
||||
.evalMap(store.fileStore.delete)
|
||||
.evalMap(store.fileRepo.delete)
|
||||
.map(_ => 1)
|
||||
.compile
|
||||
.foldMonoid
|
||||
@ -68,7 +68,7 @@ object QAttachment {
|
||||
f <-
|
||||
Stream
|
||||
.emits(files._1)
|
||||
.evalMap(store.fileStore.delete)
|
||||
.evalMap(store.fileRepo.delete)
|
||||
.map(_ => 1)
|
||||
.compile
|
||||
.foldMonoid
|
||||
@ -91,7 +91,7 @@ object QAttachment {
|
||||
f <-
|
||||
Stream
|
||||
.emits(ra.fileId +: (s.map(_.fileId).toSeq ++ p.map(_.fileId).toSeq))
|
||||
.evalMap(store.fileStore.delete)
|
||||
.evalMap(store.fileRepo.delete)
|
||||
.map(_ => 1)
|
||||
.compile
|
||||
.foldMonoid
|
||||
@ -104,7 +104,7 @@ object QAttachment {
|
||||
_ <- OptionT.liftF(
|
||||
Stream
|
||||
.emit(aa.fileId)
|
||||
.evalMap(store.fileStore.delete)
|
||||
.evalMap(store.fileRepo.delete)
|
||||
.compile
|
||||
.drain
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.common.{IdRef, _}
|
||||
import docspell.common.{FileKey, IdRef, _}
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.Store
|
||||
import docspell.store.qb.DSL._
|
||||
@ -470,7 +470,7 @@ object QItem {
|
||||
} yield tn + rn + n + mn + cf + im
|
||||
|
||||
private def findByFileIdsQuery(
|
||||
fileMetaIds: Nel[Ident],
|
||||
fileMetaIds: Nel[FileKey],
|
||||
states: Option[Nel[ItemState]]
|
||||
): Select.SimpleSelect = {
|
||||
val i = RItem.as("i")
|
||||
@ -490,7 +490,7 @@ object QItem {
|
||||
).distinct
|
||||
}
|
||||
|
||||
def findOneByFileIds(fileMetaIds: Seq[Ident]): ConnectionIO[Option[RItem]] =
|
||||
def findOneByFileIds(fileMetaIds: Seq[FileKey]): ConnectionIO[Option[RItem]] =
|
||||
Nel.fromList(fileMetaIds.toList) match {
|
||||
case Some(nel) =>
|
||||
findByFileIdsQuery(nel, None).limit(1).build.query[RItem].option
|
||||
@ -499,7 +499,7 @@ object QItem {
|
||||
}
|
||||
|
||||
def findByFileIds(
|
||||
fileMetaIds: Seq[Ident],
|
||||
fileMetaIds: Seq[FileKey],
|
||||
states: Nel[ItemState]
|
||||
): ConnectionIO[Vector[RItem]] =
|
||||
Nel.fromList(fileMetaIds.toList) match {
|
||||
@ -512,7 +512,7 @@ object QItem {
|
||||
def findByChecksum(
|
||||
checksum: String,
|
||||
collective: Ident,
|
||||
excludeFileMeta: Set[Ident]
|
||||
excludeFileMeta: Set[FileKey]
|
||||
): ConnectionIO[Vector[RItem]] = {
|
||||
val qq = findByChecksumQuery(checksum, collective, excludeFileMeta).build
|
||||
logger.debug(s"FindByChecksum: $qq")
|
||||
@ -522,7 +522,7 @@ object QItem {
|
||||
def findByChecksumQuery(
|
||||
checksum: String,
|
||||
collective: Ident,
|
||||
excludeFileMeta: Set[Ident]
|
||||
excludeFileMeta: Set[FileKey]
|
||||
): Select = {
|
||||
val m1 = RFileMeta.as("m1")
|
||||
val m2 = RFileMeta.as("m2")
|
||||
|
@ -10,7 +10,7 @@ import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.{FileKey, _}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
@ -20,7 +20,7 @@ import doobie.implicits._
|
||||
case class RAttachment(
|
||||
id: Ident,
|
||||
itemId: Ident,
|
||||
fileId: Ident,
|
||||
fileId: FileKey,
|
||||
position: Int,
|
||||
created: Timestamp,
|
||||
name: Option[String]
|
||||
@ -32,7 +32,7 @@ object RAttachment {
|
||||
|
||||
val id = Column[Ident]("attachid", this)
|
||||
val itemId = Column[Ident]("itemid", this)
|
||||
val fileId = Column[Ident]("filemetaid", this)
|
||||
val fileId = Column[FileKey]("filemetaid", this)
|
||||
val position = Column[Int]("position", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val name = Column[String]("name", this)
|
||||
@ -47,7 +47,7 @@ object RAttachment {
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.itemId},${v.fileId.id},${v.position},${v.created},${v.name}"
|
||||
fr"${v.id},${v.itemId},${v.fileId},${v.position},${v.created},${v.name}"
|
||||
)
|
||||
|
||||
def decPositions(iId: Ident, lowerBound: Int, upperBound: Int): ConnectionIO[Int] =
|
||||
@ -77,7 +77,7 @@ object RAttachment {
|
||||
|
||||
def updateFileIdAndName(
|
||||
attachId: Ident,
|
||||
fId: Ident,
|
||||
fId: FileKey,
|
||||
fname: Option[String]
|
||||
): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
@ -88,7 +88,7 @@ object RAttachment {
|
||||
|
||||
def updateFileId(
|
||||
attachId: Ident,
|
||||
fId: Ident
|
||||
fId: FileKey
|
||||
): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
@ -182,7 +182,7 @@ object RAttachment {
|
||||
def findByItemCollectiveSource(
|
||||
id: Ident,
|
||||
coll: Ident,
|
||||
fileIds: NonEmptyList[Ident]
|
||||
fileIds: NonEmptyList[FileKey]
|
||||
): ConnectionIO[Vector[RAttachment]] = {
|
||||
val i = RItem.as("i")
|
||||
val a = RAttachment.as("a")
|
||||
|
@ -8,7 +8,7 @@ package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.{FileKey, _}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb.TableDef
|
||||
import docspell.store.qb._
|
||||
@ -21,7 +21,7 @@ import doobie.implicits._
|
||||
*/
|
||||
case class RAttachmentArchive(
|
||||
id: Ident, // same as RAttachment.id
|
||||
fileId: Ident,
|
||||
fileId: FileKey,
|
||||
name: Option[String],
|
||||
messageId: Option[String],
|
||||
created: Timestamp
|
||||
@ -32,7 +32,7 @@ object RAttachmentArchive {
|
||||
val tableName = "attachment_archive"
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val fileId = Column[FileKey]("file_id", this)
|
||||
val name = Column[String]("filename", this)
|
||||
val messageId = Column[String]("message_id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
@ -59,7 +59,7 @@ object RAttachmentArchive {
|
||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.id === attachId)
|
||||
|
||||
def deleteAll(fId: Ident): ConnectionIO[Int] =
|
||||
def deleteAll(fId: FileKey): ConnectionIO[Int] =
|
||||
DML.delete(T, T.fileId === fId)
|
||||
|
||||
def findByIdAndCollective(
|
||||
|
@ -8,7 +8,7 @@ package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.{FileKey, _}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
@ -20,7 +20,7 @@ import doobie.implicits._
|
||||
*/
|
||||
case class RAttachmentPreview(
|
||||
id: Ident, // same as RAttachment.id
|
||||
fileId: Ident,
|
||||
fileId: FileKey,
|
||||
name: Option[String],
|
||||
created: Timestamp
|
||||
)
|
||||
@ -30,7 +30,7 @@ object RAttachmentPreview {
|
||||
val tableName = "attachment_preview"
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val fileId = Column[FileKey]("file_id", this)
|
||||
val name = Column[String]("filename", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
|
@ -8,7 +8,7 @@ package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.{FileKey, _}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
@ -20,7 +20,7 @@ import doobie.implicits._
|
||||
*/
|
||||
case class RAttachmentSource(
|
||||
id: Ident, // same as RAttachment.id
|
||||
fileId: Ident,
|
||||
fileId: FileKey,
|
||||
name: Option[String],
|
||||
created: Timestamp
|
||||
)
|
||||
@ -30,7 +30,7 @@ object RAttachmentSource {
|
||||
val tableName = "attachment_source"
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val fileId = Column[FileKey]("file_id", this)
|
||||
val name = Column[String]("filename", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
@ -50,7 +50,7 @@ object RAttachmentSource {
|
||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachmentSource]] =
|
||||
run(select(T.all), from(T), T.id === attachId).query[RAttachmentSource].option
|
||||
|
||||
def isSameFile(attachId: Ident, file: Ident): ConnectionIO[Boolean] =
|
||||
def isSameFile(attachId: Ident, file: FileKey): ConnectionIO[Boolean] =
|
||||
Select(count(T.id).s, from(T), T.id === attachId && T.fileId === file).build
|
||||
.query[Int]
|
||||
.unique
|
||||
|
@ -21,7 +21,7 @@ final case class RClassifierModel(
|
||||
id: Ident,
|
||||
cid: Ident,
|
||||
name: String,
|
||||
fileId: Ident,
|
||||
fileId: FileKey,
|
||||
created: Timestamp
|
||||
) {}
|
||||
|
||||
@ -30,7 +30,7 @@ object RClassifierModel {
|
||||
def createNew[F[_]: Sync](
|
||||
cid: Ident,
|
||||
name: String,
|
||||
fileId: Ident
|
||||
fileId: FileKey
|
||||
): F[RClassifierModel] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
@ -43,7 +43,7 @@ object RClassifierModel {
|
||||
val id = Column[Ident]("id", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val fileId = Column[FileKey]("file_id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all = NonEmptyList.of[Column[_]](id, cid, name, fileId, created)
|
||||
@ -61,7 +61,7 @@ object RClassifierModel {
|
||||
fr"${v.id},${v.cid},${v.name},${v.fileId},${v.created}"
|
||||
)
|
||||
|
||||
def updateFile(coll: Ident, name: String, fid: Ident): ConnectionIO[Int] =
|
||||
def updateFile(coll: Ident, name: String, fid: FileKey): ConnectionIO[Int] =
|
||||
for {
|
||||
now <- Timestamp.current[ConnectionIO]
|
||||
n <- DML.update(
|
||||
|
@ -9,7 +9,7 @@ package docspell.store.records
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.common.{FileKey, _}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
@ -18,7 +18,7 @@ import doobie.implicits._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
final case class RFileMeta(
|
||||
id: Ident,
|
||||
id: FileKey,
|
||||
created: Timestamp,
|
||||
mimetype: MimeType,
|
||||
length: ByteSize,
|
||||
@ -29,7 +29,7 @@ object RFileMeta {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "filemeta"
|
||||
|
||||
val id = Column[Ident]("file_id", this)
|
||||
val id = Column[FileKey]("file_id", this)
|
||||
val timestamp = Column[Timestamp]("created", this)
|
||||
val mimetype = Column[MimeType]("mimetype", this)
|
||||
val length = Column[ByteSize]("length", this)
|
||||
@ -47,10 +47,10 @@ object RFileMeta {
|
||||
def insert(r: RFileMeta): ConnectionIO[Int] =
|
||||
DML.insert(T, T.all, fr"${r.id},${r.created},${r.mimetype},${r.length},${r.checksum}")
|
||||
|
||||
def findById(fid: Ident): ConnectionIO[Option[RFileMeta]] =
|
||||
def findById(fid: FileKey): ConnectionIO[Option[RFileMeta]] =
|
||||
run(select(T.all), from(T), T.id === fid).query[RFileMeta].option
|
||||
|
||||
def findByIds(ids: List[Ident]): ConnectionIO[Vector[RFileMeta]] =
|
||||
def findByIds(ids: List[FileKey]): ConnectionIO[Vector[RFileMeta]] =
|
||||
NonEmptyList.fromList(ids) match {
|
||||
case Some(nel) =>
|
||||
run(select(T.all), from(T), T.id.in(nel)).query[RFileMeta].to[Vector]
|
||||
@ -58,11 +58,11 @@ object RFileMeta {
|
||||
Vector.empty[RFileMeta].pure[ConnectionIO]
|
||||
}
|
||||
|
||||
def findMime(fid: Ident): ConnectionIO[Option[MimeType]] =
|
||||
def findMime(fid: FileKey): ConnectionIO[Option[MimeType]] =
|
||||
run(select(T.mimetype), from(T), T.id === fid)
|
||||
.query[MimeType]
|
||||
.option
|
||||
|
||||
def delete(id: Ident): ConnectionIO[Int] =
|
||||
def delete(id: FileKey): ConnectionIO[Int] =
|
||||
DML.delete(T, T.id === id)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import javax.sql.DataSource
|
||||
import cats.effect._
|
||||
|
||||
import docspell.common.LenientUri
|
||||
import docspell.store.file.FileStore
|
||||
import docspell.store.file.FileRepository
|
||||
import docspell.store.impl.StoreImpl
|
||||
import docspell.store.migrate.FlywayMigrate
|
||||
|
||||
@ -67,7 +67,8 @@ object StoreFixture {
|
||||
for {
|
||||
ds <- dataSource(jdbc)
|
||||
xa <- makeXA(ds)
|
||||
store = new StoreImpl[IO](FileStore[IO](xa, ds, 64 * 1024), jdbc, xa)
|
||||
fr = FileRepository.genericJDBC[IO](xa, ds, 64 * 1024)
|
||||
store = new StoreImpl[IO](fr, jdbc, xa)
|
||||
_ <- Resource.eval(store.migrate)
|
||||
} yield store
|
||||
}
|
||||
|
Reference in New Issue
Block a user