mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-29 11:15:09 +00:00
Add a task to check for updates periodically
It must be enabled and configured by the admin. Refs: #990
This commit is contained in:
parent
90421599ea
commit
5d33b3841a
@ -21,6 +21,9 @@ import scodec.bits.ByteVector
|
||||
|
||||
object Implicits {
|
||||
|
||||
implicit val accountIdReader: ConfigReader[AccountId] =
|
||||
ConfigReader[String].emap(reason(AccountId.parse))
|
||||
|
||||
implicit val pathReader: ConfigReader[Path] =
|
||||
ConfigReader[JPath].map(Path.fromNioPath)
|
||||
|
||||
|
@ -180,6 +180,62 @@ docspell.joex {
|
||||
}
|
||||
}
|
||||
|
||||
# A periodic task to check for new releases of docspell. It can
|
||||
# inform about a new release via e-mail. You need to specify an
|
||||
# account that has SMTP settings to use for sending.
|
||||
update-check {
|
||||
# Whether to enable this task
|
||||
enabled = false
|
||||
|
||||
# Sends the mail without checking the latest release. Can be used
|
||||
# if you want to see if mail sending works, but don't want to wait
|
||||
# until a new release is published.
|
||||
test-run = false
|
||||
|
||||
# When the update check should execute. Default is to run every
|
||||
# week.
|
||||
schedule = "Sun *-*-* 00:00:00"
|
||||
|
||||
# An account id in form of `collective/user` (or just `user` if
|
||||
# collective and user name are the same). This user account must
|
||||
# have at least one valid SMTP settings which are used to send the
|
||||
# mail.
|
||||
sender-account = ""
|
||||
|
||||
# The SMTP connection id that should be used for sending the mail.
|
||||
smtp-id = ""
|
||||
|
||||
# A list of recipient e-mail addresses.
|
||||
# Example: `[ "john.doe@gmail.com" ]`
|
||||
recipients = []
|
||||
|
||||
# The subject of the mail. It supports the same variables as the
|
||||
# body.
|
||||
subject = "Docspell {{ latestVersion }} is available"
|
||||
|
||||
# The body of the mail. Subject and body can contain these
|
||||
# variables which are replaced:
|
||||
#
|
||||
# - `latestVersion` the latest available version of Docspell
|
||||
# - `currentVersion` the currently running (old) version of Docspell
|
||||
# - `releasedAt` a date when the release was published
|
||||
#
|
||||
# The body is processed as markdown after the variables have been
|
||||
# replaced.
|
||||
body = """
|
||||
Hello,
|
||||
|
||||
You are currently running Docspell {{ currentVersion }}. Version *{{ latestVersion }}*
|
||||
is now available, which was released on {{ releasedAt }}. Check the release page at:
|
||||
|
||||
<https://github.com/eikek/docspell/releases/latest>
|
||||
|
||||
Have a nice day!
|
||||
|
||||
Docpell Update Check
|
||||
"""
|
||||
}
|
||||
|
||||
# Configuration of text extraction
|
||||
extraction {
|
||||
# For PDF files it is first tried to read the text parts of the
|
||||
|
@ -19,6 +19,7 @@ import docspell.ftssolr.SolrConfig
|
||||
import docspell.joex.analysis.RegexNerFile
|
||||
import docspell.joex.hk.HouseKeepingConfig
|
||||
import docspell.joex.scheduler.{PeriodicSchedulerConfig, SchedulerConfig}
|
||||
import docspell.joex.updatecheck.UpdateCheckConfig
|
||||
import docspell.store.JdbcConfig
|
||||
|
||||
case class Config(
|
||||
@ -36,7 +37,8 @@ case class Config(
|
||||
sendMail: MailSendConfig,
|
||||
files: Files,
|
||||
mailDebug: Boolean,
|
||||
fullTextSearch: Config.FullTextSearch
|
||||
fullTextSearch: Config.FullTextSearch,
|
||||
updateCheck: UpdateCheckConfig
|
||||
)
|
||||
|
||||
object Config {
|
||||
|
@ -6,21 +6,56 @@
|
||||
|
||||
package docspell.joex
|
||||
|
||||
import cats.data.Validated
|
||||
import cats.data.ValidatedNec
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.config.Implicits._
|
||||
import docspell.joex.scheduler.CountingScheme
|
||||
|
||||
import emil.MailAddress
|
||||
import emil.javamail.syntax._
|
||||
import pureconfig._
|
||||
import pureconfig.generic.auto._
|
||||
import yamusca.imports._
|
||||
|
||||
object ConfigFile {
|
||||
import Implicits._
|
||||
|
||||
def loadConfig: Config =
|
||||
ConfigSource.default.at("docspell.joex").loadOrThrow[Config]
|
||||
validOrThrow(ConfigSource.default.at("docspell.joex").loadOrThrow[Config])
|
||||
|
||||
private def validOrThrow(cfg: Config): Config =
|
||||
validate(cfg).fold(err => sys.error(err.toList.mkString("- ", "\n", "")), identity)
|
||||
|
||||
object Implicits {
|
||||
implicit val countingSchemeReader: ConfigReader[CountingScheme] =
|
||||
ConfigReader[String].emap(reason(CountingScheme.readString))
|
||||
|
||||
implicit val templateReader: ConfigReader[Template] =
|
||||
ConfigReader[String].emap(reason(str => mustache.parse(str.trim).left.map(_._2)))
|
||||
|
||||
implicit val mailAddressReader: ConfigReader[MailAddress] =
|
||||
ConfigReader[String].emap(reason(MailAddress.parse))
|
||||
}
|
||||
|
||||
def validate(cfg: Config): ValidatedNec[String, Config] =
|
||||
List(
|
||||
failWhen(
|
||||
cfg.updateCheck.enabled && cfg.updateCheck.recipients.isEmpty,
|
||||
"No recipients given for enabled update check!"
|
||||
),
|
||||
failWhen(
|
||||
cfg.updateCheck.enabled && cfg.updateCheck.smtpId.isEmpty,
|
||||
"No recipients given for enabled update check!"
|
||||
),
|
||||
failWhen(
|
||||
cfg.updateCheck.enabled && cfg.updateCheck.subject.els.isEmpty,
|
||||
"No subject given for enabled update check!"
|
||||
)
|
||||
).reduce(_ |+| _).map(_ => cfg)
|
||||
|
||||
def failWhen(cond: Boolean, msg: => String): ValidatedNec[String, Unit] =
|
||||
Validated.condNec(!cond, (), msg)
|
||||
|
||||
}
|
||||
|
@ -32,16 +32,17 @@ import docspell.joex.process.ItemHandler
|
||||
import docspell.joex.process.ReProcessItem
|
||||
import docspell.joex.scanmailbox._
|
||||
import docspell.joex.scheduler._
|
||||
import docspell.joex.updatecheck._
|
||||
import docspell.joexapi.client.JoexClient
|
||||
import docspell.store.Store
|
||||
import docspell.store.queue._
|
||||
import docspell.store.records.{REmptyTrashSetting, RJobLog}
|
||||
import docspell.store.usertask.UserTaskScope
|
||||
import docspell.store.usertask.UserTaskStore
|
||||
|
||||
import emil.javamail._
|
||||
import org.http4s.blaze.client.BlazeClientBuilder
|
||||
import org.http4s.client.Client
|
||||
import docspell.store.usertask.UserTaskStore
|
||||
import docspell.store.usertask.UserTaskScope
|
||||
|
||||
final class JoexAppImpl[F[_]: Async](
|
||||
cfg: Config,
|
||||
@ -81,6 +82,9 @@ final class JoexAppImpl[F[_]: Async](
|
||||
.periodicTask[F](cfg.houseKeeping.schedule)
|
||||
.flatMap(pstore.insert) *>
|
||||
scheduleEmptyTrashTasks *>
|
||||
UpdateCheckTask
|
||||
.periodicTask(cfg.updateCheck)
|
||||
.flatMap(pstore.insert) *>
|
||||
MigrationTask.job.flatMap(queue.insertIfNew) *>
|
||||
AllPreviewsTask
|
||||
.job(MakePreviewArgs.StoreMode.WhenMissing, None)
|
||||
@ -130,6 +134,7 @@ object JoexAppImpl {
|
||||
itemSearchOps <- OItemSearch(store)
|
||||
analyser <- TextAnalyser.create[F](cfg.textAnalysis.textAnalysisConfig)
|
||||
regexNer <- RegexNerFile(cfg.textAnalysis.regexNerFileConfig, store)
|
||||
updateCheck <- UpdateCheck.resource(httpClient)
|
||||
javaEmil =
|
||||
JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
||||
sch <- SchedulerBuilder(cfg.scheduler, store)
|
||||
@ -239,6 +244,13 @@ object JoexAppImpl {
|
||||
EmptyTrashTask.onCancel[F]
|
||||
)
|
||||
)
|
||||
.withTask(
|
||||
JobTask.json(
|
||||
UpdateCheckTask.taskName,
|
||||
UpdateCheckTask[F](cfg.updateCheck, cfg.sendMail, javaEmil, updateCheck),
|
||||
UpdateCheckTask.onCancel[F]
|
||||
)
|
||||
)
|
||||
.resource
|
||||
psch <- PeriodicScheduler.create(
|
||||
cfg.periodicScheduler,
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.joex.updatecheck
|
||||
|
||||
import cats.effect._
|
||||
|
||||
import docspell.common.MailSendConfig
|
||||
import docspell.joex.mail.EmilHeader
|
||||
import docspell.store.records.RUserEmail
|
||||
|
||||
import emil._
|
||||
import emil.builder._
|
||||
import emil.markdown._
|
||||
import yamusca.implicits._
|
||||
import yamusca.imports._
|
||||
|
||||
object MakeMail {
|
||||
|
||||
def apply[F[_]: Sync](
|
||||
sendCfg: MailSendConfig,
|
||||
cfg: UpdateCheckConfig,
|
||||
smtpCfg: RUserEmail,
|
||||
latestRelease: UpdateCheck.Release
|
||||
): Mail[F] = {
|
||||
|
||||
val templateCtx = TemplateCtx(latestRelease)
|
||||
val md = templateCtx.render(cfg.body)
|
||||
val subj = templateCtx.render(cfg.subject)
|
||||
|
||||
MailBuilder.build(
|
||||
From(smtpCfg.mailFrom),
|
||||
Tos(cfg.recipients),
|
||||
XMailer.emil,
|
||||
Subject(subj),
|
||||
EmilHeader.listId(sendCfg.listId),
|
||||
MarkdownBody[F](md).withConfig(
|
||||
MarkdownConfig("body { font-size: 10pt; font-family: sans-serif; }")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
final case class TemplateCtx(
|
||||
currentVersion: String,
|
||||
latestVersion: String,
|
||||
releasedAt: String
|
||||
)
|
||||
object TemplateCtx {
|
||||
def apply(release: UpdateCheck.Release): TemplateCtx =
|
||||
TemplateCtx(UpdateCheck.currentVersion, release.version, release.published_at)
|
||||
|
||||
implicit val yamuscaConverter: ValueConverter[TemplateCtx] =
|
||||
ValueConverter.deriveConverter[TemplateCtx]
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.joex.updatecheck
|
||||
|
||||
import cats.effect._
|
||||
|
||||
import docspell.joex.BuildInfo
|
||||
|
||||
import io.circe.Decoder
|
||||
import io.circe.generic.semiauto._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.implicits._
|
||||
|
||||
trait UpdateCheck[F[_]] {
|
||||
|
||||
def latestRelease: F[UpdateCheck.Release]
|
||||
|
||||
}
|
||||
|
||||
object UpdateCheck {
|
||||
|
||||
val currentVersion: String =
|
||||
BuildInfo.version
|
||||
|
||||
final case class Release(
|
||||
html_url: String,
|
||||
id: Int,
|
||||
tag_name: String,
|
||||
name: String,
|
||||
created_at: String,
|
||||
published_at: String
|
||||
) {
|
||||
|
||||
def version: String = tag_name
|
||||
|
||||
def isCurrent: Boolean = {
|
||||
val version = BuildInfo.version
|
||||
version.endsWith("SNAPSHOT") || version == tag_name
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Release {
|
||||
implicit val jsonDecoder: Decoder[Release] =
|
||||
deriveDecoder[Release]
|
||||
}
|
||||
|
||||
def apply[F[_]: Async](client: Client[F]): UpdateCheck[F] =
|
||||
new UpdateCheck[F] {
|
||||
def latestRelease: F[UpdateCheck.Release] =
|
||||
client.expect[Release](latestReleaseUrl)
|
||||
}
|
||||
|
||||
def resource[F[_]: Async](client: Client[F]): Resource[F, UpdateCheck[F]] =
|
||||
Resource.pure(UpdateCheck[F](client))
|
||||
|
||||
private[this] val latestReleaseUrl =
|
||||
uri"https://api.github.com/repos/eikek/docspell/releases/latest"
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.joex.updatecheck
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
import emil.MailAddress
|
||||
import yamusca.data.Template
|
||||
|
||||
final case class UpdateCheckConfig(
|
||||
enabled: Boolean,
|
||||
testRun: Boolean,
|
||||
schedule: CalEvent,
|
||||
senderAccount: AccountId,
|
||||
smtpId: Ident,
|
||||
recipients: List[MailAddress],
|
||||
subject: Template,
|
||||
body: Template
|
||||
)
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.joex.updatecheck
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.joex.scheduler.Context
|
||||
import docspell.joex.scheduler.Task
|
||||
import docspell.store.records.RPeriodicTask
|
||||
import docspell.store.records.RUserEmail
|
||||
import docspell.store.usertask.UserTask
|
||||
import docspell.store.usertask.UserTaskScope
|
||||
|
||||
import emil._
|
||||
|
||||
object UpdateCheckTask {
|
||||
val taskName: Ident = Ident.unsafe("new-release-check")
|
||||
|
||||
type Args = Unit
|
||||
|
||||
def onCancel[F[_]]: Task[F, Args, Unit] =
|
||||
Task.log(_.warn("Cancelling update-check task"))
|
||||
|
||||
def periodicTask[F[_]: Sync](cfg: UpdateCheckConfig): F[RPeriodicTask] =
|
||||
UserTask(
|
||||
Ident.unsafe("docspell-update-check"),
|
||||
taskName,
|
||||
cfg.enabled,
|
||||
cfg.schedule,
|
||||
None,
|
||||
()
|
||||
).encode.toPeriodicTask(
|
||||
UserTaskScope(cfg.senderAccount.collective),
|
||||
"Docspell Update Check".some
|
||||
)
|
||||
|
||||
def apply[F[_]: Async](
|
||||
cfg: UpdateCheckConfig,
|
||||
sendCfg: MailSendConfig,
|
||||
emil: Emil[F],
|
||||
updateCheck: UpdateCheck[F]
|
||||
): Task[F, Args, Unit] =
|
||||
if (cfg.enabled)
|
||||
Task { ctx =>
|
||||
for {
|
||||
_ <- ctx.logger.info(
|
||||
s"Check for updates. Current version is: ${UpdateCheck.currentVersion}"
|
||||
)
|
||||
_ <- ctx.logger.debug(
|
||||
s"Get SMTP connection for ${cfg.senderAccount.asString} and ${cfg.smtpId}"
|
||||
)
|
||||
smtpCfg <- findConnection(ctx, cfg)
|
||||
_ <- ctx.logger.debug("Checking for latest release at GitHub")
|
||||
latest <- updateCheck.latestRelease
|
||||
_ <- ctx.logger.debug(s"Got latest release: $latest.")
|
||||
_ <-
|
||||
if (cfg.testRun)
|
||||
ctx.logger.info(
|
||||
s"This is a test-run as configured. A mail will always be sent!"
|
||||
)
|
||||
else ().pure[F]
|
||||
_ <-
|
||||
if (latest.isCurrent && !cfg.testRun)
|
||||
ctx.logger.info(
|
||||
s"Latest release is ${latest.version}, which is the current one. Everything uptodate."
|
||||
)
|
||||
else
|
||||
ctx.logger.info(
|
||||
s"Sending mail about new release: ${latest.tag_name}"
|
||||
) *> emil(smtpCfg.toMailConfig).send(
|
||||
MakeMail(sendCfg, cfg, smtpCfg, latest)
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
else
|
||||
Task.pure(())
|
||||
|
||||
def findConnection[F[_]: Sync](
|
||||
ctx: Context[F, _],
|
||||
cfg: UpdateCheckConfig
|
||||
): F[RUserEmail] =
|
||||
OptionT(ctx.store.transact(RUserEmail.getByName(cfg.senderAccount, cfg.smtpId)))
|
||||
.getOrElseF(
|
||||
Sync[F].raiseError(
|
||||
new Exception(
|
||||
s"No smtp connection found for user ${cfg.senderAccount.asString} and connection '${cfg.smtpId.id}'!"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -63,6 +63,27 @@ let
|
||||
min-not-found = 2;
|
||||
};
|
||||
};
|
||||
update-check = {
|
||||
enabled = false;
|
||||
test-run = false;
|
||||
schedule = "Sun *-*-* 00:00:00";
|
||||
sender-account = "";
|
||||
smtp-id = "";
|
||||
recipients = [];
|
||||
subject = "Docspell {{ latestVersion }} is available";
|
||||
body = ''
|
||||
Hello,
|
||||
|
||||
You are currently running Docspell {{ currentVersion }}. Version *{{ latestVersion }}*
|
||||
is now available, which was released on {{ releasedAt }}. Check the release page at:
|
||||
|
||||
<https://github.com/eikek/docspell/releases/latest>
|
||||
|
||||
Have a nice day!
|
||||
|
||||
Docpell Update Check
|
||||
'';
|
||||
};
|
||||
extraction = {
|
||||
pdf = {
|
||||
min-text-len = 500;
|
||||
@ -571,6 +592,88 @@ in {
|
||||
'';
|
||||
};
|
||||
|
||||
update-check = mkOption {
|
||||
type = types.submodule({
|
||||
options = {
|
||||
enabled = mkOption {
|
||||
type = types.bool;
|
||||
default = defaults.update-check.enabled;
|
||||
description = "Whether this task is enabled.";
|
||||
};
|
||||
test-run = mkOption {
|
||||
type = types.bool;
|
||||
default = defaults.update-check.test-run;
|
||||
description = ''
|
||||
Sends the mail without checking the latest release. Can be used
|
||||
if you want to see if mail sending works, but don't want to wait
|
||||
until a new release is published.
|
||||
'';
|
||||
};
|
||||
schedule = mkOption {
|
||||
type = types.str;
|
||||
default = defaults.update-check.schedule;
|
||||
description = ''
|
||||
When the check-update task should execute. Default is to run every
|
||||
week.
|
||||
'';
|
||||
};
|
||||
sender-account = mkOption {
|
||||
type = types.str;
|
||||
default = defaults.update-check.sender-account;
|
||||
description = ''
|
||||
An account id in form of `collective/user` (or just `user` if
|
||||
collective and user name are the same). This user account must
|
||||
have at least one valid SMTP settings which are used to send the
|
||||
mail.
|
||||
'';
|
||||
};
|
||||
smtp-id = mkOption {
|
||||
type = types.str;
|
||||
default = defaults.update-check.smtp-id;
|
||||
description = ''
|
||||
The SMTP connection id that should be used for sending the mail.
|
||||
'';
|
||||
};
|
||||
recipients = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = defaults.update-check.recipients;
|
||||
example = [ "josh.doe@gmail.com" ];
|
||||
description = ''
|
||||
A list of recipient e-mail addresses.
|
||||
'';
|
||||
};
|
||||
subject = mkOption {
|
||||
type = types.str;
|
||||
default = defaults.update-check.subject;
|
||||
description = ''
|
||||
The subject of the mail. It supports the same variables as the body.
|
||||
'';
|
||||
};
|
||||
body = mkOption {
|
||||
type = types.str;
|
||||
default = defaults.update-check.body;
|
||||
description = ''
|
||||
The body of the mail. Subject and body can contain these
|
||||
variables which are replaced:
|
||||
|
||||
- `latestVersion` the latest available version of Docspell
|
||||
- `currentVersion` the currently running (old) version of Docspell
|
||||
- `releasedAt` a date when the release was published
|
||||
|
||||
The body is processed as markdown after the variables have been
|
||||
replaced.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
default = defaults.update-check;
|
||||
description = ''
|
||||
A periodic task to check for new releases of docspell. It can
|
||||
inform about a new release via e-mail. You need to specify an
|
||||
account that has SMTP settings to use for sending.
|
||||
'';
|
||||
};
|
||||
|
||||
extraction = mkOption {
|
||||
type = types.submodule({
|
||||
options = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user