Restructure limits
This commit is contained in:
parent
6598ce2fe4
commit
84785b7a60
7 changed files with 105 additions and 74 deletions
|
@ -84,7 +84,7 @@ const (
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
Code string `json:"name"`
|
Code string `json:"name"`
|
||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
MessageLimit int64 `json:"messages_limit"`
|
MessagesLimit int64 `json:"messages_limit"`
|
||||||
EmailsLimit int64 `json:"emails_limit"`
|
EmailsLimit int64 `json:"emails_limit"`
|
||||||
AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"`
|
AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"`
|
||||||
AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"`
|
AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"`
|
||||||
|
|
|
@ -110,7 +110,7 @@ const (
|
||||||
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
|
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
|
||||||
)
|
)
|
||||||
|
|
||||||
// SQLiteAuthManager is an implementation of Manager and Manager. It stores users and access control list
|
// SQLiteAuthManager is an implementation of Manager. It stores users and access control list
|
||||||
// in a SQLite database.
|
// in a SQLite database.
|
||||||
type SQLiteAuthManager struct {
|
type SQLiteAuthManager struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
@ -355,7 +355,7 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) {
|
||||||
user.Plan = &Plan{
|
user.Plan = &Plan{
|
||||||
Code: planCode.String,
|
Code: planCode.String,
|
||||||
Upgradable: true, // FIXME
|
Upgradable: true, // FIXME
|
||||||
MessageLimit: messagesLimit.Int64,
|
MessagesLimit: messagesLimit.Int64,
|
||||||
EmailsLimit: emailsLimit.Int64,
|
EmailsLimit: emailsLimit.Int64,
|
||||||
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
||||||
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
||||||
|
|
|
@ -773,7 +773,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||||
contentLengthStr := r.Header.Get("Content-Length")
|
contentLengthStr := r.Header.Get("Content-Length")
|
||||||
if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below
|
if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below
|
||||||
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
||||||
if err == nil && (contentLength > visitorStats.VisitorAttachmentBytesRemaining || contentLength > s.config.AttachmentFileSizeLimit) {
|
if err == nil && (contentLength > visitorStats.AttachmentTotalSizeRemaining || contentLength > s.config.AttachmentFileSizeLimit) {
|
||||||
return errHTTPEntityTooLargeAttachmentTooLarge
|
return errHTTPEntityTooLargeAttachmentTooLarge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -791,7 +791,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
|
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
|
||||||
}
|
}
|
||||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(visitorStats.VisitorAttachmentBytesRemaining))
|
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(visitorStats.AttachmentTotalSizeRemaining))
|
||||||
if err == util.ErrLimitReached {
|
if err == util.ErrLimitReached {
|
||||||
return errHTTPEntityTooLargeAttachmentTooLarge
|
return errHTTPEntityTooLargeAttachmentTooLarge
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
|
@ -40,7 +40,21 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
response := &apiAccountSettingsResponse{
|
response := &apiAccountSettingsResponse{
|
||||||
Usage: &apiAccountStats{},
|
Stats: &apiAccountStats{
|
||||||
|
Messages: stats.Messages,
|
||||||
|
MessagesRemaining: stats.MessagesRemaining,
|
||||||
|
Emails: stats.Emails,
|
||||||
|
EmailsRemaining: stats.EmailsRemaining,
|
||||||
|
AttachmentTotalSize: stats.AttachmentTotalSize,
|
||||||
|
AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining,
|
||||||
|
},
|
||||||
|
Limits: &apiAccountLimits{
|
||||||
|
Basis: stats.Basis,
|
||||||
|
Messages: stats.MessagesLimit,
|
||||||
|
Emails: stats.EmailsLimit,
|
||||||
|
AttachmentTotalSize: stats.AttachmentTotalSizeLimit,
|
||||||
|
AttachmentFileSize: stats.AttachmentFileSizeLimit,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if v.user != nil {
|
if v.user != nil {
|
||||||
response.Username = v.user.Name
|
response.Username = v.user.Name
|
||||||
|
@ -57,62 +71,31 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v.user.Plan != nil {
|
if v.user.Plan != nil {
|
||||||
response.Usage.Basis = "account"
|
response.Plan = &apiAccountPlan{
|
||||||
response.Plan = &apiAccountSettingsPlan{
|
|
||||||
Code: v.user.Plan.Code,
|
Code: v.user.Plan.Code,
|
||||||
Upgradable: v.user.Plan.Upgradable,
|
Upgradable: v.user.Plan.Upgradable,
|
||||||
}
|
}
|
||||||
response.Limits = &apiAccountLimits{
|
|
||||||
MessagesLimit: v.user.Plan.MessageLimit,
|
|
||||||
EmailsLimit: v.user.Plan.EmailsLimit,
|
|
||||||
AttachmentFileSizeLimit: v.user.Plan.AttachmentFileSizeLimit,
|
|
||||||
AttachmentTotalSizeLimit: v.user.Plan.AttachmentTotalSizeLimit,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if v.user.Role == auth.RoleAdmin {
|
if v.user.Role == auth.RoleAdmin {
|
||||||
response.Usage.Basis = "account"
|
response.Plan = &apiAccountPlan{
|
||||||
response.Plan = &apiAccountSettingsPlan{
|
|
||||||
Code: string(auth.PlanUnlimited),
|
Code: string(auth.PlanUnlimited),
|
||||||
Upgradable: false,
|
Upgradable: false,
|
||||||
}
|
}
|
||||||
response.Limits = &apiAccountLimits{
|
|
||||||
MessagesLimit: 0,
|
|
||||||
EmailsLimit: 0,
|
|
||||||
AttachmentFileSizeLimit: 0,
|
|
||||||
AttachmentTotalSizeLimit: 0,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
response.Usage.Basis = "ip"
|
response.Plan = &apiAccountPlan{
|
||||||
response.Plan = &apiAccountSettingsPlan{
|
|
||||||
Code: string(auth.PlanDefault),
|
Code: string(auth.PlanDefault),
|
||||||
Upgradable: true,
|
Upgradable: true,
|
||||||
}
|
}
|
||||||
response.Limits = &apiAccountLimits{
|
|
||||||
MessagesLimit: int64(s.config.VisitorRequestLimitBurst),
|
|
||||||
EmailsLimit: int64(s.config.VisitorEmailLimitBurst),
|
|
||||||
AttachmentFileSizeLimit: s.config.AttachmentFileSizeLimit,
|
|
||||||
AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response.Username = auth.Everyone
|
response.Username = auth.Everyone
|
||||||
response.Role = string(auth.RoleAnonymous)
|
response.Role = string(auth.RoleAnonymous)
|
||||||
response.Usage.Basis = "ip"
|
response.Plan = &apiAccountPlan{
|
||||||
response.Plan = &apiAccountSettingsPlan{
|
|
||||||
Code: string(auth.PlanNone),
|
Code: string(auth.PlanNone),
|
||||||
Upgradable: true,
|
Upgradable: true,
|
||||||
}
|
}
|
||||||
response.Limits = &apiAccountLimits{
|
|
||||||
MessagesLimit: int64(s.config.VisitorRequestLimitBurst),
|
|
||||||
EmailsLimit: int64(s.config.VisitorEmailLimitBurst),
|
|
||||||
AttachmentFileSizeLimit: s.config.AttachmentFileSizeLimit,
|
|
||||||
AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
response.Usage.Messages = stats.Messages
|
|
||||||
response.Usage.Emails = stats.Emails
|
|
||||||
response.Usage.AttachmentsSize = stats.AttachmentBytes
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,23 +224,26 @@ type apiAccountTokenResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountSettingsPlan struct {
|
type apiAccountPlan struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountLimits struct {
|
type apiAccountLimits struct {
|
||||||
MessagesLimit int64 `json:"messages"`
|
Basis string `json:"basis"` // "ip", "role" or "plan"
|
||||||
EmailsLimit int64 `json:"emails"`
|
Messages int64 `json:"messages"`
|
||||||
AttachmentFileSizeLimit int64 `json:"attachment_file_size"`
|
Emails int64 `json:"emails"`
|
||||||
AttachmentTotalSizeLimit int64 `json:"attachment_total_size"`
|
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
||||||
|
AttachmentFileSize int64 `json:"attachment_file_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountStats struct {
|
type apiAccountStats struct {
|
||||||
Basis string `json:"basis"` // "ip" or "account"
|
|
||||||
Messages int64 `json:"messages"`
|
Messages int64 `json:"messages"`
|
||||||
|
MessagesRemaining int64 `json:"messages_remaining"`
|
||||||
Emails int64 `json:"emails"`
|
Emails int64 `json:"emails"`
|
||||||
AttachmentsSize int64 `json:"attachments_size"`
|
EmailsRemaining int64 `json:"emails_remaining"`
|
||||||
|
AttachmentTotalSize int64 `json:"attachment_total_size"`
|
||||||
|
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountSettingsResponse struct {
|
type apiAccountSettingsResponse struct {
|
||||||
|
@ -249,7 +252,7 @@ type apiAccountSettingsResponse struct {
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
Notification *auth.UserNotificationPrefs `json:"notification,omitempty"`
|
Notification *auth.UserNotificationPrefs `json:"notification,omitempty"`
|
||||||
Subscriptions []*auth.UserSubscription `json:"subscriptions,omitempty"`
|
Subscriptions []*auth.UserSubscription `json:"subscriptions,omitempty"`
|
||||||
Plan *apiAccountSettingsPlan `json:"plan,omitempty"`
|
Plan *apiAccountPlan `json:"plan,omitempty"`
|
||||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||||
Usage *apiAccountStats `json:"usage,omitempty"`
|
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,17 +40,27 @@ type visitor struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type visitorStats struct {
|
type visitorStats struct {
|
||||||
|
Basis string // "ip", "role" or "plan"
|
||||||
Messages int64
|
Messages int64
|
||||||
|
MessagesLimit int64
|
||||||
|
MessagesRemaining int64
|
||||||
Emails int64
|
Emails int64
|
||||||
AttachmentBytes int64
|
EmailsLimit int64
|
||||||
|
EmailsRemaining int64
|
||||||
|
AttachmentTotalSize int64
|
||||||
|
AttachmentTotalSizeLimit int64
|
||||||
|
AttachmentTotalSizeRemaining int64
|
||||||
|
AttachmentFileSizeLimit int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor {
|
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor {
|
||||||
var requestLimiter *rate.Limiter
|
var requestLimiter, emailsLimiter *rate.Limiter
|
||||||
if user != nil && user.Plan != nil {
|
if user != nil && user.Plan != nil {
|
||||||
requestLimiter = rate.NewLimiter(rate.Limit(user.Plan.MessageLimit)*rate.Every(24*time.Hour), conf.VisitorRequestLimitBurst)
|
requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.MessagesLimit), conf.VisitorRequestLimitBurst)
|
||||||
|
emailsLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.EmailsLimit), conf.VisitorEmailLimitBurst)
|
||||||
} else {
|
} else {
|
||||||
requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst)
|
requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst)
|
||||||
|
emailsLimiter = rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst)
|
||||||
}
|
}
|
||||||
return &visitor{
|
return &visitor{
|
||||||
config: conf,
|
config: conf,
|
||||||
|
@ -60,7 +70,7 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *a
|
||||||
messages: 0, // TODO
|
messages: 0, // TODO
|
||||||
emails: 0, // TODO
|
emails: 0, // TODO
|
||||||
requestLimiter: requestLimiter,
|
requestLimiter: requestLimiter,
|
||||||
emailsLimiter: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
emailsLimiter: emailsLimiter,
|
||||||
subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
|
subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
|
||||||
bandwidthLimiter: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour),
|
bandwidthLimiter: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour),
|
||||||
firebase: time.Unix(0, 0),
|
firebase: time.Unix(0, 0),
|
||||||
|
@ -147,9 +157,46 @@ func (v *visitor) Stats() (*visitorStats, error) {
|
||||||
}
|
}
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
return &visitorStats{
|
stats := &visitorStats{}
|
||||||
Messages: v.messages,
|
if v.user != nil && v.user.Role == auth.RoleAdmin {
|
||||||
Emails: v.emails,
|
stats.Basis = "role"
|
||||||
AttachmentBytes: attachmentsBytesUsed,
|
stats.MessagesLimit = 0
|
||||||
}, nil
|
stats.EmailsLimit = 0
|
||||||
|
stats.AttachmentTotalSizeLimit = 0
|
||||||
|
stats.AttachmentFileSizeLimit = 0
|
||||||
|
} else if v.user != nil && v.user.Plan != nil {
|
||||||
|
stats.Basis = "plan"
|
||||||
|
stats.MessagesLimit = v.user.Plan.MessagesLimit
|
||||||
|
stats.EmailsLimit = v.user.Plan.EmailsLimit
|
||||||
|
stats.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit
|
||||||
|
stats.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit
|
||||||
|
} else {
|
||||||
|
stats.Basis = "ip"
|
||||||
|
stats.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish)
|
||||||
|
stats.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish)
|
||||||
|
stats.AttachmentTotalSizeLimit = v.config.AttachmentTotalSizeLimit
|
||||||
|
stats.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
|
||||||
|
}
|
||||||
|
stats.Messages = v.messages
|
||||||
|
stats.MessagesRemaining = zeroIfNegative(stats.MessagesLimit - stats.MessagesLimit)
|
||||||
|
stats.Emails = v.emails
|
||||||
|
stats.EmailsRemaining = zeroIfNegative(stats.EmailsLimit - stats.EmailsRemaining)
|
||||||
|
stats.AttachmentTotalSize = attachmentsBytesUsed
|
||||||
|
stats.AttachmentTotalSizeRemaining = zeroIfNegative(stats.AttachmentTotalSizeLimit - stats.AttachmentTotalSize)
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func zeroIfNegative(value int64) int64 {
|
||||||
|
if value < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func replenishDurationToDailyLimit(duration time.Duration) int64 {
|
||||||
|
return int64(24 * time.Hour / duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dailyLimitToRate(limit int64) rate.Limit {
|
||||||
|
return rate.Limit(limit) * rate.Every(24*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,6 @@ const Stats = () => {
|
||||||
return <></>; // TODO loading
|
return <></>; // TODO loading
|
||||||
}
|
}
|
||||||
const accountType = account.plan.code ?? "none";
|
const accountType = account.plan.code ?? "none";
|
||||||
const limits = account.limits;
|
|
||||||
const usage = account.usage;
|
|
||||||
const normalize = (value, max) => (value / max * 100);
|
const normalize = (value, max) => (value / max * 100);
|
||||||
return (
|
return (
|
||||||
<Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}>
|
<Card sx={{p: 3}} aria-label={t("xxxxxxxxx")}>
|
||||||
|
@ -78,24 +76,24 @@ const Stats = () => {
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref labelId={"messages"} title={t("Published messages")}>
|
<Pref labelId={"messages"} title={t("Published messages")}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{usage.messages}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{limits.messages > 0 ? t("of {{limit}}", { limit: limits.messages }) : t("Unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("of {{limit}}", { limit: account.limits.messages }) : t("Unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={limits.messages > 0 ? normalize(usage.messages, limits.messages) : 100} />
|
<LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref labelId={"emails"} title={t("Emails sent")}>
|
<Pref labelId={"emails"} title={t("Emails sent")}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{usage.emails}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{limits.emails > 0 ? t("of {{limit}}", { limit: limits.emails }) : t("Unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("of {{limit}}", { limit: account.limits.emails }) : t("Unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={limits.emails > 0 ? normalize(usage.emails, limits.emails) : 100} />
|
<LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
|
||||||
</Pref>
|
</Pref>
|
||||||
<Pref labelId={"attachments"} title={t("Attachment storage")}>
|
<Pref labelId={"attachments"} title={t("Attachment storage")}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(usage.attachments_size)}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(limits.attachment_total_size) }) : t("Unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(account.limits.attachment_total_size) }) : t("Unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress variant="determinate" value={limits.attachment_total_size > 0 ? normalize(usage.attachments_size, limits.attachment_total_size) : 100} />
|
<LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
|
||||||
</Pref>
|
</Pref>
|
||||||
</PrefGroup>
|
</PrefGroup>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
Loading…
Reference in a new issue