Use a minimum age of items to remove

In order to keep deleted items for a while, the periodic task can now
use a duration to only remove items with a certain age. This can be
used to ensure that a deleted item stays at least X days before it
will be removed from the database.

Refs: #347
This commit is contained in:
eikek
2021-08-15 12:28:42 +02:00
parent d136bb8166
commit f4a2b86ea8
27 changed files with 303 additions and 124 deletions

View File

@ -0,0 +1,8 @@
ALTER TABLE "empty_trash_setting"
ADD COLUMN "min_age" bigint;
UPDATE "empty_trash_setting"
SET "min_age" = 604800000;
ALTER TABLE "empty_trash_setting"
ALTER COLUMN "min_age" SET NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE `empty_trash_setting`
ADD COLUMN (`min_age` bigint);
UPDATE `empty_trash_setting`
SET `min_age` = 604800000;
ALTER TABLE `empty_trash_setting`
MODIFY `min_age` bigint NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE "empty_trash_setting"
ADD COLUMN "min_age" bigint;
UPDATE "empty_trash_setting"
SET "min_age" = 604800000;
ALTER TABLE "empty_trash_setting"
ALTER COLUMN "min_age" SET NOT NULL;

View File

@ -34,6 +34,9 @@ trait DoobieMeta extends EmilDoobieMeta {
e.apply(a).noSpaces
)
implicit val metaDuration: Meta[Duration] =
Meta[Long].imap(Duration.millis)(_.millis)
implicit val metaCollectiveState: Meta[CollectiveState] =
Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString)

View File

@ -54,15 +54,23 @@ object QUserTask {
)
).query[RPeriodicTask].option.map(_.map(makeUserTask))
def insert(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
def insert(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[String]
): ConnectionIO[Int] =
for {
r <- task.toPeriodicTask[ConnectionIO](scope)
r <- task.toPeriodicTask[ConnectionIO](scope, subject)
n <- RPeriodicTask.insert(r)
} yield n
def update(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
def update(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[String]
): ConnectionIO[Int] =
for {
r <- task.toPeriodicTask[ConnectionIO](scope)
r <- task.toPeriodicTask[ConnectionIO](scope, subject)
n <- RPeriodicTask.update(r)
} yield n

View File

@ -13,7 +13,6 @@ import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import com.github.eikek.calev._
import doobie._
import doobie.implicits._
@ -75,16 +74,15 @@ object RCollective {
)
)
now <- Timestamp.current[ConnectionIO]
cls = settings.classifier.map(_.toRecord(cid, now))
n2 <- cls match {
case Some(cr) =>
RClassifierSetting.update(cr)
n2 <- settings.classifier match {
case Some(cls) =>
RClassifierSetting.update(cls.toRecord(cid, now))
case None =>
RClassifierSetting.delete(cid)
}
n3 <- settings.emptyTrash match {
case Some(trashSchedule) =>
REmptyTrashSetting.update(REmptyTrashSetting(cid, trashSchedule, now))
case Some(trash) =>
REmptyTrashSetting.update(trash.toRecord(cid, now))
case None =>
REmptyTrashSetting.delete(cid)
}
@ -114,7 +112,8 @@ object RCollective {
cs.itemCount.s,
cs.categories.s,
cs.listType.s,
es.schedule.s
es.schedule.s,
es.minAge.s
),
from(c).leftJoin(cs, cs.cid === c.id).leftJoin(es, es.cid === c.id),
c.id === coll
@ -168,7 +167,7 @@ object RCollective {
language: Language,
integrationEnabled: Boolean,
classifier: Option[RClassifierSetting.Classifier],
emptyTrash: Option[CalEvent]
emptyTrash: Option[REmptyTrashSetting.EmptyTrash]
)
}

View File

@ -21,6 +21,7 @@ import doobie.implicits._
final case class REmptyTrashSetting(
cid: Ident,
schedule: CalEvent,
minAge: Duration,
created: Timestamp
)
@ -31,8 +32,9 @@ object REmptyTrashSetting {
val cid = Column[Ident]("cid", this)
val schedule = Column[CalEvent]("schedule", this)
val minAge = Column[Duration]("min_age", this)
val created = Column[Timestamp]("created", this)
val all = NonEmptyList.of[Column[_]](cid, schedule, created)
val all = NonEmptyList.of[Column[_]](cid, schedule, minAge, created)
}
val T = Table(None)
@ -43,7 +45,7 @@ object REmptyTrashSetting {
DML.insert(
T,
T.all,
fr"${v.cid},${v.schedule},${v.created}"
fr"${v.cid},${v.schedule},${v.minAge},${v.created}"
)
def update(v: REmptyTrashSetting): ConnectionIO[Int] =
@ -52,7 +54,8 @@ object REmptyTrashSetting {
T,
T.cid === v.cid,
DML.set(
T.schedule.setTo(v.schedule)
T.schedule.setTo(v.schedule),
T.minAge.setTo(v.minAge)
)
)
n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO]
@ -64,7 +67,7 @@ object REmptyTrashSetting {
}
def findForAllCollectives(
default: CalEvent,
default: EmptyTrash,
chunkSize: Int
): Stream[ConnectionIO, REmptyTrashSetting] = {
val c = RCollective.as("c")
@ -72,7 +75,8 @@ object REmptyTrashSetting {
val sql = run(
select(
c.id.s,
coalesce(e.schedule.s, const(default)).s,
coalesce(e.schedule.s, const(default.schedule)).s,
coalesce(e.minAge.s, const(default.minAge)).s,
coalesce(e.created.s, c.created.s).s
),
from(c).leftJoin(e, e.cid === c.id)
@ -83,4 +87,13 @@ object REmptyTrashSetting {
def delete(coll: Ident): ConnectionIO[Int] =
DML.delete(T, T.cid === coll)
final case class EmptyTrash(schedule: CalEvent, minAge: Duration) {
def toRecord(coll: Ident, created: Timestamp): REmptyTrashSetting =
REmptyTrashSetting(coll, schedule, minAge, created)
}
object EmptyTrash {
val default = EmptyTrash(EmptyTrashArgs.defaultSchedule, Duration.days(7))
def fromRecord(r: REmptyTrashSetting): EmptyTrash =
EmptyTrash(r.schedule, r.minAge)
}
}

View File

@ -389,8 +389,16 @@ object RItem {
def findById(itemId: Ident): ConnectionIO[Option[RItem]] =
run(select(T.all), from(T), T.id === itemId).query[RItem].option
def findDeleted(collective: Ident, chunkSize: Int): Stream[ConnectionIO, RItem] =
run(select(T.all), from(T), T.cid === collective && T.state === ItemState.deleted)
def findDeleted(
collective: Ident,
maxUpdated: Timestamp,
chunkSize: Int
): Stream[ConnectionIO, RItem] =
run(
select(T.all),
from(T),
T.cid === collective && T.state === ItemState.deleted && T.updated < maxUpdated
)
.query[RItem]
.streamWithChunkSize(chunkSize)

View File

@ -43,7 +43,8 @@ object UserTask {
.map(a => ut.copy(args = a))
def toPeriodicTask[F[_]: Sync](
scope: UserTaskScope
scope: UserTaskScope,
subject: Option[String]
): F[RPeriodicTask] =
RPeriodicTask
.create[F](
@ -51,7 +52,7 @@ object UserTask {
scope,
ut.name,
ut.args,
s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}",
subject.getOrElse(s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}"),
Priority.Low,
ut.timer,
ut.summary

View File

@ -61,7 +61,9 @@ trait UserTaskStore[F[_]] {
* exists, a new one is created. Otherwise the existing task is
* updated.
*/
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
def updateTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
implicit E: Encoder[A]
): F[Int]
/** Delete the task with the given id of the given user.
*/
@ -92,8 +94,8 @@ trait UserTaskStore[F[_]] {
* the user `account`, they will all be removed and the given task
* inserted!
*/
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
def updateOneTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
implicit E: Encoder[A]
): F[UserTask[String]]
/** Delete all tasks of the given user that have name `name'.
@ -123,16 +125,16 @@ object UserTaskStore {
case Left(err) => Stream.raiseError[F](new Exception(err))
})
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
def updateTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
implicit E: Encoder[A]
): F[Int] = {
val exists = QUserTask.exists(ut.id)
val insert = QUserTask.insert(scope, ut.encode)
val insert = QUserTask.insert(scope, subject, ut.encode)
store.add(insert, exists).flatMap {
case AddResult.Success =>
1.pure[F]
case AddResult.EntityExists(_) =>
store.transact(QUserTask.update(scope, ut.encode))
store.transact(QUserTask.update(scope, subject, ut.encode))
case AddResult.Failure(ex) =>
Async[F].raiseError(ex)
}
@ -166,21 +168,25 @@ object UserTaskStore {
case Left(err) => Async[F].raiseError(new Exception(err))
})
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
def updateOneTask[A](
scope: UserTaskScope,
subject: Option[String],
ut: UserTask[A]
)(implicit
E: Encoder[A]
): F[UserTask[String]] =
getByNameRaw(scope, ut.name).compile.toList.flatMap {
case a :: rest =>
val task = ut.copy(id = a.id).encode
for {
_ <- store.transact(QUserTask.update(scope, task))
_ <- store.transact(QUserTask.update(scope, subject, task))
_ <- store.transact(
rest.traverse(t => QUserTask.delete(scope.toAccountId, t.id))
)
} yield task
case Nil =>
val task = ut.encode
store.transact(QUserTask.insert(scope, task)).map(_ => task)
store.transact(QUserTask.insert(scope, subject, task)).map(_ => task)
}
def deleteAll(scope: UserTaskScope, name: Ident): F[Int] =