File indexing completed on 2024-05-19 16:31:38
0001 /* 0002 * SPDX-FileCopyrightText: 2015 Dominik Haumann <dhaumann@kde.org> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.1-or-later 0005 */ 0006 #include "DiskQuota.h" 0007 #include "QuotaItem.h" 0008 0009 #include <KFormat> 0010 #include <KLocalizedString> 0011 0012 #include <QRegularExpression> 0013 #include <QStandardPaths> 0014 #include <QTimer> 0015 // #include <QDebug> 0016 0017 DiskQuota::DiskQuota(QObject *parent) 0018 : QObject(parent) 0019 , m_timer(new QTimer(this)) 0020 , m_quotaProcess(new QProcess(this)) 0021 , m_model(new QuotaListModel(this)) 0022 { 0023 connect(m_timer, &QTimer::timeout, this, &DiskQuota::updateQuota); 0024 m_timer->start(2 * 60 * 1000); // check every 2 minutes 0025 0026 connect(m_quotaProcess, (void (QProcess::*)(int, QProcess::ExitStatus)) & QProcess::finished, this, &DiskQuota::quotaProcessFinished); 0027 0028 updateQuota(); 0029 } 0030 0031 bool DiskQuota::quotaInstalled() const 0032 { 0033 return m_quotaInstalled; 0034 } 0035 0036 void DiskQuota::setQuotaInstalled(bool installed) 0037 { 0038 if (m_quotaInstalled != installed) { 0039 m_quotaInstalled = installed; 0040 0041 if (!installed) { 0042 m_model->clear(); 0043 setStatus(PassiveStatus); 0044 setToolTip(i18n("Disk Quota")); 0045 setSubToolTip(i18n("Please install 'quota'")); 0046 } 0047 0048 Q_EMIT quotaInstalledChanged(); 0049 } 0050 } 0051 0052 bool DiskQuota::cleanUpToolInstalled() const 0053 { 0054 return m_cleanUpToolInstalled; 0055 } 0056 0057 void DiskQuota::setCleanUpToolInstalled(bool installed) 0058 { 0059 if (m_cleanUpToolInstalled != installed) { 0060 m_cleanUpToolInstalled = installed; 0061 Q_EMIT cleanUpToolInstalledChanged(); 0062 } 0063 } 0064 0065 DiskQuota::TrayStatus DiskQuota::status() const 0066 { 0067 return m_status; 0068 } 0069 0070 void DiskQuota::setStatus(TrayStatus status) 0071 { 0072 if (m_status != status) { 0073 m_status = status; 0074 Q_EMIT statusChanged(); 0075 } 0076 } 0077 0078 QString DiskQuota::iconName() const 0079 { 0080 return m_iconName; 0081 } 0082 0083 void DiskQuota::setIconName(const QString &name) 0084 { 0085 if (m_iconName != name) { 0086 m_iconName = name; 0087 Q_EMIT iconNameChanged(); 0088 } 0089 } 0090 0091 QString DiskQuota::toolTip() const 0092 { 0093 return m_toolTip; 0094 } 0095 0096 void DiskQuota::setToolTip(const QString &toolTip) 0097 { 0098 if (m_toolTip != toolTip) { 0099 m_toolTip = toolTip; 0100 Q_EMIT toolTipChanged(); 0101 } 0102 } 0103 0104 QString DiskQuota::subToolTip() const 0105 { 0106 return m_subToolTip; 0107 } 0108 0109 void DiskQuota::setSubToolTip(const QString &subToolTip) 0110 { 0111 if (m_subToolTip != subToolTip) { 0112 m_subToolTip = subToolTip; 0113 Q_EMIT subToolTipChanged(); 0114 } 0115 } 0116 0117 static QString iconNameForQuota(int quota) 0118 { 0119 if (quota < 50) { 0120 return QStringLiteral("disk-quota"); 0121 } else if (quota < 75) { 0122 return QStringLiteral("disk-quota-low"); 0123 } else if (quota < 90) { 0124 return QStringLiteral("disk-quota-high"); 0125 } 0126 0127 // quota >= 90% 0128 return QStringLiteral("disk-quota-critical"); 0129 } 0130 0131 static bool isQuotaLine(const QString &line) 0132 { 0133 const int iMax = line.size(); 0134 for (int i = 0; i < iMax; ++i) { 0135 if (!line[i].isSpace() && line[i] == QLatin1Char('/')) { 0136 return true; 0137 } 0138 } 0139 return false; 0140 } 0141 0142 void DiskQuota::updateQuota() 0143 { 0144 const bool quotaFound = !QStandardPaths::findExecutable(QStringLiteral("quota")).isEmpty(); 0145 setQuotaInstalled(quotaFound); 0146 if (!quotaFound) { 0147 return; 0148 } 0149 0150 // for now, only filelight is supported 0151 setCleanUpToolInstalled(!QStandardPaths::findExecutable(QStringLiteral("filelight")).isEmpty()); 0152 0153 // kill running process in case it hanged for whatever reason 0154 if (m_quotaProcess->state() != QProcess::NotRunning) { 0155 m_quotaProcess->kill(); 0156 } 0157 0158 // Try to run 'quota' 0159 const QStringList args{ 0160 QStringLiteral("--show-mntpoint"), // second entry is e.g. '/home' 0161 QStringLiteral("--hide-device"), // hide e.g. /dev/sda3 0162 QStringLiteral("--no-mixed-pathnames"), // trim leading slashes from NFSv4 mountpoints 0163 QStringLiteral("--all-nfs"), // show all mount points 0164 QStringLiteral("--no-wrap"), // do not wrap long lines 0165 QStringLiteral("--quiet-refuse"), // no not print error message when NFS server does not respond 0166 }; 0167 0168 m_quotaProcess->start(QStringLiteral("quota"), args, QIODevice::ReadOnly); 0169 } 0170 0171 void DiskQuota::quotaProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) 0172 { 0173 Q_UNUSED(exitCode) 0174 0175 if (exitStatus != QProcess::NormalExit) { 0176 m_model->clear(); 0177 setToolTip(i18n("Disk Quota")); 0178 setSubToolTip(i18n("Running quota failed")); 0179 return; 0180 } 0181 0182 // get quota output 0183 const QString rawData = QString::fromLocal8Bit(m_quotaProcess->readAllStandardOutput()); 0184 // qDebug() << rawData; 0185 0186 const QStringList lines = rawData.split(QRegularExpression(QStringLiteral("[\r\n]")), Qt::SkipEmptyParts); 0187 // Testing 0188 // QStringList lines = QStringList() 0189 // << QStringLiteral("/home/peterpan 3975379* 5000000 7000000 57602 0 0") 0190 // << QStringLiteral("/home/archive 2263536 6000000 5100000 3932 0 0") 0191 // << QStringLiteral("/home/shared 4271196* 10000000 7000000 57602 0 0"); 0192 // << QStringLiteral("/home/peterpan %1* 5000000 7000000 57602 0 0").arg(qrand() % 5000000) 0193 // << QStringLiteral("/home/archive %1 5000000 5100000 3932 0 0").arg(qrand() % 5000000) 0194 // << QStringLiteral("/home/shared %1* 5000000 7000000 57602 0 0").arg(qrand() % 5000000); 0195 // lines.removeAt(qrand() % lines.size()); 0196 0197 // format class needed for GiB/MiB/KiB formatting 0198 KFormat fmt; 0199 int maxQuota = 0; 0200 QVector<QuotaItem> items; 0201 0202 // assumption: Filesystem starts with slash 0203 for (const QString &line : lines) { 0204 // qDebug() << line << isQuotaLine(line); 0205 if (!isQuotaLine(line)) { 0206 continue; 0207 } 0208 0209 QStringList parts = line.split(QLatin1Char(' '), Qt::SkipEmptyParts); 0210 // valid lines range from 7 to 9 parts (grace not always there): 0211 // Disk quotas for user dh (uid 1000): 0212 // Filesystem blocks quota limit grace files quota limit grace 0213 // /home 16296500 50000000 60000000 389155 0 0 0214 // /home 16296500* 50000000 60000000 6 389155 0 0 0215 // /home 16296500* 50000000 60000000 4 389155 0 0 5 0216 // ^...........we want these...........^ 0217 // NOTE: In case of a soft limit violation, a '*' is added in the used blocks. 0218 // Hence, the star is removed below, if applicable 0219 0220 if (parts.size() < 4) { 0221 continue; 0222 } 0223 0224 // 'quota' uses kilo bytes -> factor 1024 0225 // NOTE: int is not large enough, hence qint64 0226 const qint64 used = parts[1].remove(QLatin1Char('*')).toLongLong() * 1024; 0227 qint64 softLimit = parts[2].toLongLong() * 1024; 0228 const qint64 hardLimit = parts[3].toLongLong() * 1024; 0229 if (softLimit == 0) { // softLimit might be unused (0) 0230 softLimit = hardLimit; 0231 } 0232 const qint64 freeSize = softLimit - used; 0233 const int percent = qMin(100, qMax(0, qRound(used * 100.0 / softLimit))); 0234 0235 QuotaItem item; 0236 item.setIconName(iconNameForQuota(percent)); 0237 item.setMountPoint(parts[0]); 0238 item.setUsage(percent); 0239 item.setMountString(i18nc("usage of quota, e.g.: '/home/bla: 38% used'", "%1: %2% used", parts[0], percent)); 0240 item.setUsedString(i18nc("e.g.: 12 GiB of 20 GiB", "%1 of %2", fmt.formatByteSize(used), fmt.formatByteSize(softLimit))); 0241 item.setFreeString(i18nc("e.g.: 8 GiB free", "%1 free", fmt.formatByteSize(qMax(qint64(0), freeSize)))); 0242 0243 items.append(item); 0244 0245 maxQuota = qMax(maxQuota, percent); 0246 } 0247 0248 // qDebug() << "QUOTAS:" << quotas; 0249 0250 // make sure max quota is 100. Could be more, due to the 0251 // hard limit > soft limit, and we take soft limit as 100% 0252 maxQuota = qMin(100, maxQuota); 0253 0254 // update icon in panel 0255 setIconName(iconNameForQuota(maxQuota)); 0256 0257 // update status 0258 setStatus(maxQuota < 50 ? PassiveStatus : maxQuota < 98 ? ActiveStatus : NeedsAttentionStatus); 0259 0260 if (!items.isEmpty()) { 0261 setToolTip(i18nc("example: Quota: 83% used", "Quota: %1% used", maxQuota)); 0262 setSubToolTip(QString()); 0263 } else { 0264 setToolTip(i18n("Disk Quota")); 0265 setSubToolTip(i18n("No quota restrictions found.")); 0266 } 0267 0268 // merge new items, add new ones, remove old ones 0269 m_model->updateItems(items); 0270 } 0271 0272 QuotaListModel *DiskQuota::model() const 0273 { 0274 return m_model; 0275 } 0276 0277 void DiskQuota::openCleanUpTool(const QString &mountPoint) 0278 { 0279 if (!cleanUpToolInstalled()) { 0280 return; 0281 } 0282 0283 QProcess::startDetached(QStringLiteral("filelight"), {mountPoint}); 0284 }