mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-21 09:58:26 +00:00
Add route to check for files by their checksum
Adopt scripts in `tools/` to check for existing files using these routes.
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package docspell.backend.ops
|
||||
|
||||
import fs2.Stream
|
||||
import cats.data.OptionT
|
||||
import cats.implicits._
|
||||
import cats.effect.{Effect, Resource}
|
||||
import doobie._
|
||||
@ -11,6 +12,7 @@ import OItem.{AttachmentData, ItemData, ListItem, Query}
|
||||
import bitpeace.{FileMeta, RangeDef}
|
||||
import docspell.common.{Direction, Ident, ItemState, MetaProposalList, Timestamp}
|
||||
import docspell.store.records.{RAttachment, RAttachmentMeta, RItem, RTagItem}
|
||||
import docspell.store.records.RSource
|
||||
|
||||
trait OItem[F[_]] {
|
||||
|
||||
@ -47,6 +49,11 @@ trait OItem[F[_]] {
|
||||
def delete(itemId: Ident, collective: Ident): F[Int]
|
||||
|
||||
def findAttachmentMeta(id: Ident, collective: Ident): F[Option[RAttachmentMeta]]
|
||||
|
||||
def findByFileCollective(checksum: String, collective: Ident): F[Vector[RItem]]
|
||||
|
||||
def findByFileSource(checksum: String, sourceId: Ident): F[Vector[RItem]]
|
||||
|
||||
}
|
||||
|
||||
object OItem {
|
||||
@ -163,5 +170,15 @@ object OItem {
|
||||
|
||||
def findAttachmentMeta(id: Ident, collective: Ident): F[Option[RAttachmentMeta]] =
|
||||
store.transact(QAttachment.getAttachmentMeta(id, collective))
|
||||
|
||||
def findByFileCollective(checksum: String, collective: Ident): F[Vector[RItem]] =
|
||||
store.transact(QItem.findByChecksum(checksum, collective))
|
||||
|
||||
def findByFileSource(checksum: String, sourceId: Ident): F[Vector[RItem]] =
|
||||
store.transact((for {
|
||||
coll <- OptionT(RSource.findCollective(sourceId))
|
||||
items <- OptionT.liftF(QItem.findByChecksum(checksum, coll))
|
||||
} yield items).getOrElse(Vector.empty))
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -47,6 +47,26 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthResult"
|
||||
/open/checkfile/{id}/{checksum}:
|
||||
get:
|
||||
tags: [ Upload ]
|
||||
summary: Check if a file is in docspell.
|
||||
description: |
|
||||
Checks if a file with the given SHA-256 checksum is in
|
||||
docspell. The id is a *source id* configured by a collective.
|
||||
|
||||
The result shows all items that contains a file with the given
|
||||
checksum.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/checksum"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CheckFileResult"
|
||||
/open/upload/item/{id}:
|
||||
post:
|
||||
tags: [ Upload ]
|
||||
@ -95,6 +115,25 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/checkfile/{checksum}:
|
||||
get:
|
||||
tags: [ Upload ]
|
||||
summary: Check if a file is in docspell.
|
||||
description: |
|
||||
Checks if a file with the given SHA-256 checksum is in
|
||||
docspell.
|
||||
|
||||
The result shows all items that contains a file with the given
|
||||
checksum.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/checksum"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CheckFileResult"
|
||||
/sec/upload:
|
||||
post:
|
||||
tags: [ Upload ]
|
||||
@ -1169,6 +1208,46 @@ paths:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
components:
|
||||
schemas:
|
||||
CheckFileResult:
|
||||
description: |
|
||||
Results when searching for file checksums.
|
||||
required:
|
||||
- exists
|
||||
- items
|
||||
properties:
|
||||
exists:
|
||||
type: boolean
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/BasicItem"
|
||||
BasicItem:
|
||||
description: |
|
||||
Basic properties about an item.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- direction
|
||||
- state
|
||||
- created
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
name:
|
||||
type: string
|
||||
direction:
|
||||
type: string
|
||||
format: direction
|
||||
state:
|
||||
type: string
|
||||
format: itemstate
|
||||
created:
|
||||
type: integer
|
||||
format: date-time
|
||||
itemDate:
|
||||
type: integer
|
||||
format: date-time
|
||||
GenInvite:
|
||||
description: |
|
||||
A request to generate a new invitation key.
|
||||
@ -2083,3 +2162,10 @@ components:
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
checksum:
|
||||
name: checksum
|
||||
in: path
|
||||
description: A SHA-256 checksum
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
@ -31,7 +31,7 @@ object RestServer {
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||
securedRoutes(cfg, restApp, token)
|
||||
},
|
||||
"/api/doc" -> templates.doc,
|
||||
"/api/doc" -> templates.doc,
|
||||
"/app/assets" -> WebjarRoutes.appRoutes[F](blocker),
|
||||
"/app" -> templates.app
|
||||
).orNotFound
|
||||
@ -68,13 +68,15 @@ object RestServer {
|
||||
"queue" -> JobQueueRoutes(restApp.backend, token),
|
||||
"item" -> ItemRoutes(restApp.backend, token),
|
||||
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token)
|
||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
||||
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token)
|
||||
)
|
||||
|
||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
Router(
|
||||
"auth" -> LoginRoutes.login(restApp.backend.login, cfg),
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg)
|
||||
"auth" -> LoginRoutes.login(restApp.backend.login, cfg),
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
||||
"checkfile" -> CheckFileRoutes.open(restApp.backend)
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.Ident
|
||||
import docspell.restapi.model.{BasicItem, CheckFileResult}
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import docspell.store.records.RItem
|
||||
|
||||
object CheckFileRoutes {
|
||||
|
||||
def secured[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / checksum =>
|
||||
for {
|
||||
items <- backend.item.findByFileCollective(checksum, user.account.collective)
|
||||
resp <- Ok(convert(items))
|
||||
} yield resp
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def open[F[_]: Effect](backend: BackendApp[F]): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / Ident(id) / checksum =>
|
||||
for {
|
||||
items <- backend.item.findByFileSource(checksum, id)
|
||||
resp <- Ok(convert(items))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
private def convert(v: Vector[RItem]): CheckFileResult =
|
||||
CheckFileResult(
|
||||
v.nonEmpty,
|
||||
v.map(r => BasicItem(r.id, r.name, r.direction, r.state, r.created, r.itemDate)).toList
|
||||
)
|
||||
|
||||
}
|
@ -5,6 +5,7 @@ import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.{Ident, Priority}
|
||||
import docspell.restapi.model.BasicResult
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
@ -36,6 +37,9 @@ object UploadRoutes {
|
||||
res <- Ok(basicResult(result))
|
||||
} yield res
|
||||
|
||||
case GET -> Root / "checkfile" / checksum =>
|
||||
Ok(BasicResult(false, s"not implemented $checksum"))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +55,9 @@ object UploadRoutes {
|
||||
result <- backend.upload.submit(updata, id)
|
||||
res <- Ok(basicResult(result))
|
||||
} yield res
|
||||
|
||||
case GET -> Root / "checkfile" / Ident(id) / checksum =>
|
||||
Ok(BasicResult(false, s"not implemented $id $checksum"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +239,19 @@ object QItem {
|
||||
q.query[RItem].to[Vector]
|
||||
}
|
||||
|
||||
def findByChecksum(checksum: String, collective: Ident): ConnectionIO[Vector[RItem]] = {
|
||||
val IC = RItem.Columns.all.map(_.prefix("i"))
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
|
||||
val from = RItem.table ++ fr"i INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ aItem.is(iId) ++
|
||||
fr"INNER JOIN filemeta m ON m.id = a.filemetaid"
|
||||
selectSimple(IC, from, and(fr"m.checksum = $checksum", iColl.is(collective)))
|
||||
.query[RItem]
|
||||
.to[Vector]
|
||||
}
|
||||
|
||||
private def queryWildcard(value: String): String = {
|
||||
def prefix(n: String) =
|
||||
if (n.startsWith("*")) s"%${n.substring(1)}"
|
||||
|
Reference in New Issue
Block a user