mirror of
https://github.com/chatmail/core.git
synced 2026-05-22 08:16:32 +03:00
add device message if quota is exceeding (#2621)
* resultify update_recent_quota()
* add a device-message if quota exceeds QUOTA_WARN_THRESHOLD_PERCENTAGE
* check if a quota warning should be added during housekeeping, this is at least once a day
* dc_get_config("quota_exceeding") is useful for bots
* make clippy happy
* reword QuotaExceedingMsgBody message
* avoid lots of warnings if quota jitters around the warning threshold
* allow clippy::assertions_on_constants
these constants depend on each other, it makes sense to check that they are not changed in an incompatible way
This commit is contained in:
@@ -377,6 +377,9 @@ int dc_set_config (dc_context_t* context, const char*
|
|||||||
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
|
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
|
||||||
* - `sys.config_keys` = get a space-separated list of all config-keys available.
|
* - `sys.config_keys` = get a space-separated list of all config-keys available.
|
||||||
* The config-keys are the keys that can be passed to the parameter `key` of this function.
|
* The config-keys are the keys that can be passed to the parameter `key` of this function.
|
||||||
|
* - `quota_exceeding` = 0: quota is unknown or in normal range;
|
||||||
|
* >=80: quota is about to exceed, the value is the concrete percentage,
|
||||||
|
* a device message is added when that happens, however, that value may still be interesting for bots.
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object. For querying system values, this can be NULL.
|
* @param context The context object. For querying system values, this can be NULL.
|
||||||
@@ -5675,6 +5678,13 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used in message summary text for notifications and chatlist.
|
/// Used in message summary text for notifications and chatlist.
|
||||||
#define DC_STR_FORWARDED 97
|
#define DC_STR_FORWARDED 97
|
||||||
|
|
||||||
|
/// "Quota exceeding, already %1$s%% used."
|
||||||
|
///
|
||||||
|
/// Used as device message text.
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by the percentage used
|
||||||
|
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ pub enum Config {
|
|||||||
#[strum(props(default = "0"))]
|
#[strum(props(default = "0"))]
|
||||||
NotifyAboutWrongPw,
|
NotifyAboutWrongPw,
|
||||||
|
|
||||||
|
/// If a warning about exceeding quota was shown recently,
|
||||||
|
/// this is the percentage of quota at the time the warning was given.
|
||||||
|
/// Unset, when quota falls below minimal warning threshold again.
|
||||||
|
QuotaExceeding,
|
||||||
|
|
||||||
/// address to webrtc instance to use for videochats
|
/// address to webrtc instance to use for videochats
|
||||||
WebrtcInstance,
|
WebrtcInstance,
|
||||||
|
|
||||||
|
|||||||
@@ -423,6 +423,12 @@ impl Context {
|
|||||||
.await?
|
.await?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
res.insert(
|
||||||
|
"quota_exceeding",
|
||||||
|
self.get_config_int(Config::QuotaExceeding)
|
||||||
|
.await?
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
let elapsed = self.creation_time.elapsed();
|
let elapsed = self.creation_time.elapsed();
|
||||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||||
|
|||||||
@@ -1152,7 +1152,10 @@ async fn perform_job_action(
|
|||||||
sql::housekeeping(context).await.ok_or_log(context);
|
sql::housekeeping(context).await.ok_or_log(context);
|
||||||
Status::Finished(Ok(()))
|
Status::Finished(Ok(()))
|
||||||
}
|
}
|
||||||
Action::UpdateRecentQuota => context.update_recent_quota(connection.inbox()).await,
|
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(err) => Status::Finished(Err(err)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||||
|
|||||||
86
src/quota.rs
86
src/quota.rs
@@ -4,13 +4,17 @@ use anyhow::{anyhow, Result};
|
|||||||
use async_imap::types::{Quota, QuotaResource};
|
use async_imap::types::{Quota, QuotaResource};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
|
use crate::chat::add_device_msg_with_importance;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::Viewtype;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::dc_tools::time;
|
use crate::dc_tools::time;
|
||||||
use crate::imap::scan_folders::get_watched_folders;
|
use crate::imap::scan_folders::get_watched_folders;
|
||||||
use crate::imap::Imap;
|
use crate::imap::Imap;
|
||||||
use crate::job::{Action, Status};
|
use crate::job::{Action, Status};
|
||||||
|
use crate::message::Message;
|
||||||
use crate::param::Params;
|
use crate::param::Params;
|
||||||
use crate::{job, EventType};
|
use crate::{job, stock_str, EventType};
|
||||||
|
|
||||||
/// warn about a nearly full mailbox after this usage percentage is reached.
|
/// warn about a nearly full mailbox after this usage percentage is reached.
|
||||||
/// quota icon is "yellow".
|
/// quota icon is "yellow".
|
||||||
@@ -20,6 +24,12 @@ pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
|
|||||||
// this threshold only makes the quota icon "red".
|
// this threshold only makes the quota icon "red".
|
||||||
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99;
|
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99;
|
||||||
|
|
||||||
|
/// if quota is below this value (again),
|
||||||
|
/// QuotaExceeding is cleared.
|
||||||
|
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
|
||||||
|
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
|
||||||
|
pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
|
||||||
|
|
||||||
// if recent quota is older,
|
// if recent quota is older,
|
||||||
// it is re-fetched on dc_get_connectivity_html()
|
// it is re-fetched on dc_get_connectivity_html()
|
||||||
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
|
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
|
||||||
@@ -63,6 +73,29 @@ async fn get_unique_quota_roots_and_usage(
|
|||||||
Ok(unique_quota_roots)
|
Ok(unique_quota_roots)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_highest_usage<'t>(
|
||||||
|
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
|
||||||
|
) -> Result<(u64, &'t String, &QuotaResource)> {
|
||||||
|
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
|
||||||
|
for (name, resources) in unique_quota_roots {
|
||||||
|
for r in resources {
|
||||||
|
let usage_percent = r.get_usage_percentage();
|
||||||
|
match highest {
|
||||||
|
None => {
|
||||||
|
highest = Some((usage_percent, name, r));
|
||||||
|
}
|
||||||
|
Some((up, ..)) => {
|
||||||
|
if up <= usage_percent {
|
||||||
|
highest = Some((usage_percent, name, r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
highest.ok_or_else(|| anyhow!("no quota_resource found, this is unexpected"))
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
// Adds a job to update `quota.recent`
|
// Adds a job to update `quota.recent`
|
||||||
pub(crate) async fn schedule_quota_update(&self) {
|
pub(crate) async fn schedule_quota_update(&self) {
|
||||||
@@ -77,11 +110,17 @@ impl Context {
|
|||||||
/// Updates `quota.recent`, sets `quota.modified` to the current time
|
/// Updates `quota.recent`, sets `quota.modified` to the current time
|
||||||
/// and emits an event to let the UIs update connectivity view.
|
/// and emits an event to let the UIs update connectivity view.
|
||||||
///
|
///
|
||||||
|
/// Moreover, once each time quota gets larger than `QUOTA_WARN_THRESHOLD_PERCENTAGE`,
|
||||||
|
/// a device message is added.
|
||||||
|
/// As the message is added only once, the user is not spammed
|
||||||
|
/// in case for some providers the quota is always at ~100%
|
||||||
|
/// and new space is allocated as needed.
|
||||||
|
///
|
||||||
/// Called in response to `Action::UpdateRecentQuota`.
|
/// Called in response to `Action::UpdateRecentQuota`.
|
||||||
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Status {
|
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<Status> {
|
||||||
if let Err(err) = imap.prepare(self).await {
|
if let Err(err) = imap.prepare(self).await {
|
||||||
warn!(self, "could not connect: {:?}", err);
|
warn!(self, "could not connect: {:?}", err);
|
||||||
return Status::RetryNow;
|
return Ok(Status::RetryNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
let quota = if imap.can_check_quota() {
|
let quota = if imap.can_check_quota() {
|
||||||
@@ -91,12 +130,51 @@ impl Context {
|
|||||||
Err(anyhow!("Quota not supported by your provider."))
|
Err(anyhow!("Quota not supported by your provider."))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Ok(quota) = "a {
|
||||||
|
match get_highest_usage(quota) {
|
||||||
|
Ok((highest, _, _)) => {
|
||||||
|
if highest >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
|
||||||
|
if self.get_config_int(Config::QuotaExceeding).await? == 0 {
|
||||||
|
self.set_config(Config::QuotaExceeding, Some(&highest.to_string()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
msg.text = Some(stock_str::quota_exceeding(self, highest).await);
|
||||||
|
add_device_msg_with_importance(self, None, Some(&mut msg), true)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
|
||||||
|
self.set_config(Config::QuotaExceeding, None).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => warn!(self, "cannot get highest quota usage: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
*self.quota.write().await = Some(QuotaInfo {
|
*self.quota.write().await = Some(QuotaInfo {
|
||||||
recent: quota,
|
recent: quota,
|
||||||
modified: time(),
|
modified: time(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.emit_event(EventType::ConnectivityChanged);
|
self.emit_event(EventType::ConnectivityChanged);
|
||||||
Status::Finished(Ok(()))
|
Ok(Status::Finished(Ok(())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::quota::{
|
||||||
|
QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE,
|
||||||
|
QUOTA_WARN_THRESHOLD_PERCENTAGE,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::assertions_on_constants)]
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_quota_thresholds() -> anyhow::Result<()> {
|
||||||
|
assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
|
||||||
|
assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE);
|
||||||
|
assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
|
||||||
|
assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -598,6 +598,8 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.schedule_quota_update().await;
|
||||||
|
|
||||||
if let Err(e) = context
|
if let Err(e) = context
|
||||||
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
|
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -258,6 +258,15 @@ pub enum StockMessage {
|
|||||||
|
|
||||||
#[strum(props(fallback = "Forwarded"))]
|
#[strum(props(fallback = "Forwarded"))]
|
||||||
Forwarded = 97,
|
Forwarded = 97,
|
||||||
|
|
||||||
|
#[strum(props(
|
||||||
|
fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
|
||||||
|
You may not be able to receive message when the storage is 100%% used.\n\n\
|
||||||
|
👉 Please check if you can delete old data in the provider's webinterface \
|
||||||
|
and consider to enable \"Settings / Delete Old Messages\". \
|
||||||
|
You can check your current storage usage anytime at \"Settings / Connectivity\"."
|
||||||
|
))]
|
||||||
|
QuotaExceedingMsgBody = 98,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StockMessage {
|
impl StockMessage {
|
||||||
@@ -840,6 +849,14 @@ pub(crate) async fn forwarded(context: &Context) -> String {
|
|||||||
translated(context, StockMessage::Forwarded).await
|
translated(context, StockMessage::Forwarded).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
|
||||||
|
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
|
||||||
|
translated(context, StockMessage::QuotaExceedingMsgBody)
|
||||||
|
.await
|
||||||
|
.replace1(format!("{}", highest_usage))
|
||||||
|
.replace("%%", "%")
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Set the stock string for the [StockMessage].
|
/// Set the stock string for the [StockMessage].
|
||||||
///
|
///
|
||||||
@@ -1023,6 +1040,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_quota_exceeding_stock_str() -> anyhow::Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
let str = quota_exceeding(&t, 81).await;
|
||||||
|
assert!(str.contains("81% "));
|
||||||
|
assert!(str.contains("100% "));
|
||||||
|
assert!(!str.contains("%%"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn test_update_device_chats() {
|
async fn test_update_device_chats() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user