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 }