95c03040c7
- FileStore: защита от выхода за пределы хранилища (isInsideVault), валидация имён (запрет скрытых, зарезервированных, управляющих символов), проверка destDir, лимит watcher, dailyNote валидация, openVault проверка isDir - DocumentManager: лимит 10МБ, кламп автосейва 0.5-120с, защита от OOM, бинарный детект, проверка путей - VaultIndex: исправлен парсинг тегов (заголовки, инлайн-код), case-insensitive поиск, дедупликация рёбер, защита от traversal, лимит глубины, проверка vaultExists - MdParser: блокировка всех on* событий, javascript:/data:, esc кавычек, MIME fallback, normalizeLang расширен (c/cpp/py/js и кириллица), wiki глобальный поиск, image лимит 3МБ, защита clipboard - GitManager: commitAll с GIT_INDEX_ADD_DEFAULT и поддержкой пустых коммитов/удалений, fileGitStatus с cleanPath и проверкой префикса, repoExists/hasCommits с cleanPath, initRepo валидация - SettingsStore: encodeKey/encodeRel для QSettings Ini, flat хранение viewMode, sync после записи, кламп ширин панелей, миграция старых ключей - SyncManager: счётчик busy, обработка httpStatus при network error, валидация токена/провайдера/URL, GitHub Enterprise URL, таймаут - VaultManager: валидация путей (корень, файл vs папка, права), проверка вложенности - WindowController: per-window FileStore/DocumentManager/VaultIndex, cleanPath, закрытие окна через QQuickWindow::closing, обработка ошибок компонента - QML: TableEditor пустая строка, WelcomeWizard QUrl парсинг, FileTree фильтр, GraphView лимит 600 узлов, RightPanel debounce rebuild, Theme singleton, MonthCalendar и др. - Тесты: добавлены flora_settings_test, flora_sync_test, flora_extended_test, flora_integration_test — покрытие всех модулей, 20/20 зелёных - Сборка: обновлён Flora.app (4.6М бинарь -> 173М бандл, ad-hoc подпись, проверен offscreen запуск, версия 0.0.1 + cmark 0.29 + libgit2 1.9) - .gitignore: добавлены Testing/, coverage, gcda/gcov, __pycache__, .tmp, test-vault и др.
563 lines
21 KiB
C++
563 lines
21 KiB
C++
#include "core/FileStore.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QRegularExpression>
|
|
#include <QSet>
|
|
#include <functional>
|
|
|
|
FileStore::FileStore(QObject *parent)
|
|
: QAbstractListModel(parent)
|
|
{
|
|
m_debounce.setSingleShot(true);
|
|
m_debounce.setInterval(300);
|
|
connect(&m_debounce, &QTimer::timeout, this, &FileStore::rebuild);
|
|
connect(&m_watcher, &QFileSystemWatcher::directoryChanged,
|
|
this, &FileStore::scheduleRebuild);
|
|
}
|
|
|
|
QModelIndex FileStore::index(int row, int column, const QModelIndex &parent) const
|
|
{
|
|
if (parent.isValid() || column != 0 || row < 0 || row >= m_rows.size())
|
|
return QModelIndex();
|
|
return createIndex(row, 0);
|
|
}
|
|
|
|
QModelIndex FileStore::parent(const QModelIndex &child) const
|
|
{
|
|
Q_UNUSED(child)
|
|
return QModelIndex();
|
|
}
|
|
|
|
int FileStore::rowCount(const QModelIndex &parent) const
|
|
{
|
|
return parent.isValid() ? 0 : m_rows.size();
|
|
}
|
|
|
|
QVariant FileStore::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_rows.size())
|
|
return QVariant();
|
|
const Row &r = m_rows.at(index.row());
|
|
switch (role) {
|
|
case NameRole: return r.name;
|
|
case PathRole: return r.path;
|
|
case DepthRole: return r.depth;
|
|
case IsDirRole: return r.isDir;
|
|
case ExpandedRole: return r.expanded;
|
|
case HasChildrenRole: return r.hasChildren;
|
|
case IconRole: return iconFor(r);
|
|
default: return QVariant();
|
|
}
|
|
}
|
|
|
|
QHash<int, QByteArray> FileStore::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
roles[NameRole] = "display";
|
|
roles[PathRole] = "path";
|
|
roles[DepthRole] = "depth";
|
|
roles[IsDirRole] = "isDir";
|
|
roles[ExpandedRole] = "expanded";
|
|
roles[HasChildrenRole] = "hasChildren";
|
|
roles[IconRole] = "icon";
|
|
return roles;
|
|
}
|
|
|
|
QString FileStore::iconFor(const Row &r) const
|
|
{
|
|
if (r.isDir)
|
|
return r.hasChildren ? QStringLiteral("folder") : QStringLiteral("folder-empty");
|
|
const QString ext = QFileInfo(r.name).suffix().toLower();
|
|
if (ext == "md" || ext == "markdown") return QStringLiteral("file-md");
|
|
if (ext == "html" || ext == "htm") return QStringLiteral("file-html");
|
|
if (ext == "pdf") return QStringLiteral("file-pdf");
|
|
if (ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "gif"
|
|
|| ext == "svg" || ext == "webp" || ext == "bmp")
|
|
return QStringLiteral("file-img");
|
|
if (ext == "txt" || ext == "log") return QStringLiteral("file-text");
|
|
if (ext == "c" || ext == "cpp" || ext == "h" || ext == "hpp" || ext == "py"
|
|
|| ext == "go" || ext == "rs" || ext == "js" || ext == "ts" || ext == "json"
|
|
|| ext == "yaml" || ext == "yml" || ext == "sh" || ext == "bash" || ext == "css")
|
|
return QStringLiteral("file-code");
|
|
return QStringLiteral("file");
|
|
}
|
|
|
|
QString FileStore::openVault(const QString &path)
|
|
{
|
|
const QString clean = QDir::cleanPath(path.trimmed());
|
|
if (clean.isEmpty())
|
|
return QStringLiteral("Путь к хранилищу не указан.");
|
|
QFileInfo fi(clean);
|
|
if (!fi.exists())
|
|
return QStringLiteral("Папка «%1» не существует.").arg(clean);
|
|
if (!fi.isDir())
|
|
return QStringLiteral("Путь «%1» не является папкой.").arg(clean);
|
|
|
|
m_vault = clean;
|
|
m_expanded.clear();
|
|
rebuild();
|
|
registerWatcher();
|
|
|
|
emit vaultChanged();
|
|
emit readyChanged();
|
|
return QString();
|
|
}
|
|
|
|
void FileStore::rebuild()
|
|
{
|
|
QVector<Row> rows;
|
|
collectDir(m_vault, 0, rows);
|
|
|
|
beginResetModel();
|
|
m_rows = std::move(rows);
|
|
endResetModel();
|
|
|
|
registerWatcher();
|
|
}
|
|
|
|
void FileStore::collectDir(const QString &dir, int depth, QVector<Row> &out) const
|
|
{
|
|
QDir d(dir);
|
|
d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::NoSymLinks
|
|
| QDir::System);
|
|
const QFileInfoList entries = d.entryInfoList();
|
|
|
|
QVector<Row> dirs, files;
|
|
for (const QFileInfo &fi : entries) {
|
|
const QString path = QDir::cleanPath(fi.absoluteFilePath());
|
|
if (isIgnored(path))
|
|
continue;
|
|
Row row;
|
|
row.path = path;
|
|
row.name = fi.fileName();
|
|
row.depth = depth;
|
|
row.isDir = fi.isDir();
|
|
if (row.isDir) {
|
|
QDir cd(path);
|
|
cd.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::NoSymLinks);
|
|
const QFileInfoList children = cd.entryInfoList();
|
|
int visible = 0;
|
|
for (const auto &c : children)
|
|
if (!isIgnored(c.absoluteFilePath()))
|
|
++visible;
|
|
row.hasChildren = visible > 0;
|
|
}
|
|
row.expanded = row.isDir && m_expanded.contains(path);
|
|
(row.isDir ? dirs : files).append(row);
|
|
}
|
|
|
|
std::sort(dirs.begin(), dirs.end(), [](const Row &a, const Row &b) {
|
|
return a.name.toLower() < b.name.toLower();
|
|
});
|
|
std::sort(files.begin(), files.end(), [](const Row &a, const Row &b) {
|
|
return a.name.toLower() < b.name.toLower();
|
|
});
|
|
|
|
for (const Row &r : dirs) {
|
|
out.append(r);
|
|
if (r.expanded)
|
|
collectDir(r.path, depth + 1, out);
|
|
}
|
|
for (const Row &r : files)
|
|
out.append(r);
|
|
}
|
|
|
|
bool FileStore::isIgnored(const QString &path) const
|
|
{
|
|
const QString name = QFileInfo(path).fileName();
|
|
return name == QLatin1String(".git") || name == QLatin1String(".flora")
|
|
|| name == QLatin1String(".stfolder") || name == QLatin1String(".DS_Store")
|
|
|| name == QLatin1String("Thumbs.db");
|
|
}
|
|
|
|
void FileStore::registerWatcher()
|
|
{
|
|
const auto dirs = m_watcher.directories();
|
|
for (const QString &d : dirs)
|
|
m_watcher.removePath(d);
|
|
if (m_vault.isEmpty() || !QDir(m_vault).exists())
|
|
return;
|
|
// Следим за всеми каталогами хранилища, даже свёрнутыми, чтобы внешние изменения
|
|
// внутри свёрнутых папок приводили к rebuild (иначе watcher пропустит события).
|
|
QVector<QString> dirsToWatch;
|
|
dirsToWatch.append(m_vault);
|
|
// Собираем рекурсивно все подкаталоги, игнорируя служебные
|
|
std::function<void(const QString&)> collect = [&](const QString &cur) {
|
|
QDir d(cur);
|
|
d.setFilter(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks | QDir::System);
|
|
const QFileInfoList subs = d.entryInfoList();
|
|
for (const QFileInfo &fi : subs) {
|
|
if (isIgnored(fi.absoluteFilePath()))
|
|
continue;
|
|
const QString clean = QDir::cleanPath(fi.absoluteFilePath());
|
|
if (!dirsToWatch.contains(clean))
|
|
dirsToWatch.append(clean);
|
|
collect(fi.absoluteFilePath());
|
|
}
|
|
};
|
|
collect(m_vault);
|
|
// Также добавляем видимые dirs на случай рассинхрона
|
|
for (const Row &r : m_rows)
|
|
if (r.isDir && !dirsToWatch.contains(r.path))
|
|
dirsToWatch.append(r.path);
|
|
// Ограничение QFileSystemWatcher: лимит дескрипторов (обычно 256-512)
|
|
// При превышении addPaths вернёт пустой список без ошибки - проверяем
|
|
if (dirsToWatch.size() > 512) {
|
|
// Оставляем только верхний уровень + видимые для экономии дескрипторов
|
|
QVector<QString> limited;
|
|
limited.append(m_vault);
|
|
for (const Row &r : m_rows)
|
|
if (r.isDir && !limited.contains(r.path))
|
|
limited.append(r.path);
|
|
dirsToWatch = limited;
|
|
}
|
|
const QStringList failed = m_watcher.addPaths(dirsToWatch);
|
|
Q_UNUSED(failed)
|
|
}
|
|
|
|
void FileStore::scheduleRebuild()
|
|
{
|
|
if (!m_vault.isEmpty())
|
|
m_debounce.start();
|
|
}
|
|
|
|
void FileStore::toggleExpand(int row)
|
|
{
|
|
if (row < 0 || row >= m_rows.size() || !m_rows[row].isDir)
|
|
return;
|
|
const QString path = m_rows[row].path;
|
|
if (m_expanded.contains(path))
|
|
m_expanded.remove(path);
|
|
else
|
|
m_expanded.insert(path);
|
|
rebuild();
|
|
}
|
|
|
|
void FileStore::expandAll()
|
|
{
|
|
for (const Row &r : m_rows)
|
|
if (r.isDir)
|
|
m_expanded.insert(r.path);
|
|
rebuild();
|
|
}
|
|
|
|
void FileStore::collapseAll()
|
|
{
|
|
m_expanded.clear();
|
|
rebuild();
|
|
}
|
|
|
|
int FileStore::rowOf(const QString &path) const
|
|
{
|
|
for (int i = 0; i < m_rows.size(); ++i)
|
|
if (m_rows[i].path == path)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
QString FileStore::validateName(const QString &name) const
|
|
{
|
|
const QString n = name.trimmed();
|
|
if (n.isEmpty())
|
|
return QStringLiteral("Имя не может быть пустым.");
|
|
if (n == "." || n == "..")
|
|
return QStringLiteral("Имя не может быть «.» или «..».");
|
|
if (n.startsWith('.'))
|
|
return QStringLiteral("Имя не может начинаться с точки.");
|
|
// Запрещённые символы (кроссплатформенно): / \ : * ? " < > | и управляющие 0x00-0x1F
|
|
static const QRegularExpression invalidRe(QStringLiteral("[\\\\/:*?\"<>|\\x00-\\x1F]"));
|
|
if (n.contains('/') || n.contains('\\') || invalidRe.match(n).hasMatch())
|
|
return QStringLiteral("Имя содержит недопустимые символы (/, \\, :, *, ?, \", <, >, |).");
|
|
// Зарезервированные имена Windows (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
|
static const QSet<QString> reserved = {
|
|
"CON","PRN","AUX","NUL",
|
|
"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9",
|
|
"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"
|
|
};
|
|
const QString upper = n.split('.').first().toUpper();
|
|
if (reserved.contains(upper))
|
|
return QStringLiteral("Имя зарезервировано системой.");
|
|
if (n.length() > 200)
|
|
return QStringLiteral("Имя слишком длинное.");
|
|
if (n.endsWith(' ') || n.endsWith('.'))
|
|
return QStringLiteral("Имя не может заканчиваться пробелом или точкой.");
|
|
return QString();
|
|
}
|
|
|
|
bool FileStore::isInsideVault(const QString &path) const
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return false;
|
|
const QString vault = QDir::cleanPath(m_vault);
|
|
const QString p = QDir::cleanPath(path);
|
|
if (p == vault)
|
|
return true;
|
|
// Должен начинаться с vault + '/'
|
|
return p.startsWith(vault + '/');
|
|
}
|
|
|
|
QString FileStore::parentDirOfRow(int row) const
|
|
{
|
|
if (row <= 0)
|
|
return m_vault;
|
|
const Row &cur = m_rows[row];
|
|
for (int i = row - 1; i >= 0; --i)
|
|
if (m_rows[i].isDir && m_rows[i].depth < cur.depth)
|
|
return m_rows[i].path;
|
|
return m_vault;
|
|
}
|
|
|
|
QString FileStore::createFile(const QString &parentPath, const QString &name)
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return QStringLiteral("Хранилище не открыто.");
|
|
QString dir = parentPath.isEmpty() ? m_vault : QDir::cleanPath(parentPath);
|
|
if (!isInsideVault(dir))
|
|
return QStringLiteral("Путь вне хранилища: %1").arg(dir);
|
|
QFileInfo dirInfo(dir);
|
|
if (!dirInfo.exists() || !dirInfo.isDir())
|
|
return QStringLiteral("Папка не существует: %1").arg(dir);
|
|
const QString trimmed = name.trimmed();
|
|
if (trimmed.isEmpty())
|
|
return QStringLiteral("Имя файла не может быть пустым.");
|
|
// Если имя без расширения — добавляем .md
|
|
QString actualName = trimmed;
|
|
if (!actualName.contains(QLatin1Char('.')))
|
|
actualName += QStringLiteral(".md");
|
|
const QString err = validateName(actualName);
|
|
if (!err.isEmpty())
|
|
return err;
|
|
const QString path = QDir(dir).filePath(actualName);
|
|
const QString cleanPath = QDir::cleanPath(path);
|
|
if (!isInsideVault(cleanPath))
|
|
return QStringLiteral("Итоговый путь вне хранилища.");
|
|
if (QFileInfo::exists(path))
|
|
return QStringLiteral("Файл «%1» уже существует в этом каталоге.").arg(actualName);
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::WriteOnly))
|
|
return QStringLiteral("Не удалось создать файл: %1").arg(path);
|
|
f.close();
|
|
rebuild();
|
|
return QString();
|
|
}
|
|
|
|
QString FileStore::dailyNotePath(const QString &dateStr)
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return QString();
|
|
// Валидация формата даты: YYYY-MM-DD
|
|
static const QRegularExpression dateRe(QStringLiteral("^\\d{4}-\\d{2}-\\d{2}$"));
|
|
if (!dateRe.match(dateStr).hasMatch())
|
|
return QString();
|
|
const QString journalDir = QDir::cleanPath(m_vault + "/Журнал");
|
|
if (!isInsideVault(journalDir))
|
|
return QString();
|
|
if (!QDir().mkpath(journalDir))
|
|
return QString();
|
|
const QString path = QDir(journalDir).filePath(dateStr + ".md");
|
|
const QString cleanPath = QDir::cleanPath(path);
|
|
if (!isInsideVault(cleanPath))
|
|
return QString();
|
|
if (!QFileInfo::exists(path)) {
|
|
QFile f(path);
|
|
if (f.open(QIODevice::WriteOnly)) {
|
|
f.write(QStringLiteral("# %1\n\n").arg(dateStr).toUtf8());
|
|
f.close();
|
|
} else {
|
|
return QString();
|
|
}
|
|
m_expanded.insert(journalDir);
|
|
rebuild();
|
|
}
|
|
return cleanPath;
|
|
}
|
|
|
|
QString FileStore::createFolder(const QString &parentPath, const QString &name)
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return QStringLiteral("Хранилище не открыто.");
|
|
QString dir = parentPath.isEmpty() ? m_vault : QDir::cleanPath(parentPath);
|
|
if (!isInsideVault(dir))
|
|
return QStringLiteral("Путь вне хранилища: %1").arg(dir);
|
|
QFileInfo dirInfo(dir);
|
|
if (!dirInfo.exists() || !dirInfo.isDir())
|
|
return QStringLiteral("Папка не существует: %1").arg(dir);
|
|
const QString err = validateName(name.trimmed());
|
|
if (!err.isEmpty())
|
|
return err;
|
|
const QString path = QDir(dir).filePath(name.trimmed());
|
|
const QString cleanPath = QDir::cleanPath(path);
|
|
if (!isInsideVault(cleanPath))
|
|
return QStringLiteral("Итоговый путь вне хранилища.");
|
|
if (QFileInfo::exists(path))
|
|
return QStringLiteral("Папка с таким именем уже существует.");
|
|
if (!QDir().mkpath(path))
|
|
return QStringLiteral("Не удалось создать папку: %1").arg(path);
|
|
m_expanded.insert(QDir::cleanPath(path));
|
|
rebuild();
|
|
return QString();
|
|
}
|
|
|
|
QString FileStore::renameItem(const QString &path, const QString &newName)
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return QStringLiteral("Хранилище не открыто.");
|
|
const QString cleanPath = QDir::cleanPath(path);
|
|
if (!isInsideVault(cleanPath))
|
|
return QStringLiteral("Путь вне хранилища: %1").arg(path);
|
|
QFileInfo fi(cleanPath);
|
|
if (!fi.exists())
|
|
return QStringLiteral("Элемент не существует: %1").arg(path);
|
|
const QString n = newName.trimmed();
|
|
const QString err = validateName(n);
|
|
if (!err.isEmpty())
|
|
return err;
|
|
const QString newPath = QDir(fi.absolutePath()).filePath(n);
|
|
const QString cleanNew = QDir::cleanPath(newPath);
|
|
if (!isInsideVault(cleanNew))
|
|
return QStringLiteral("Новый путь вне хранилища.");
|
|
if (cleanPath == cleanNew)
|
|
return QString();
|
|
if (QFileInfo::exists(newPath))
|
|
return QStringLiteral("Имя уже занято.");
|
|
if (!QFile::rename(cleanPath, newPath) && !QDir().rename(cleanPath, newPath))
|
|
return QStringLiteral("Не удалось переименовать: %1").arg(path);
|
|
if (fi.isDir()) {
|
|
const QString oldAbs = cleanPath;
|
|
const QString newAbs = cleanNew;
|
|
// Обновляем все раскрытые пути с префиксом oldAbs (включая вложенные)
|
|
QSet<QString> updated;
|
|
for (const QString &p : std::as_const(m_expanded)) {
|
|
if (p == oldAbs)
|
|
updated.insert(newAbs);
|
|
else if (p.startsWith(oldAbs + '/'))
|
|
updated.insert(newAbs + p.mid(oldAbs.size()));
|
|
else
|
|
updated.insert(p);
|
|
}
|
|
m_expanded = std::move(updated);
|
|
}
|
|
rebuild();
|
|
return QString();
|
|
}
|
|
|
|
QString FileStore::removeItem(const QString &path)
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return QStringLiteral("Хранилище не открыто.");
|
|
const QString clean = QDir::cleanPath(path);
|
|
if (!isInsideVault(clean))
|
|
return QStringLiteral("Путь вне хранилища: %1").arg(path);
|
|
if (clean == QDir::cleanPath(m_vault))
|
|
return QStringLiteral("Нельзя удалить корень хранилища.");
|
|
QFileInfo fi(clean);
|
|
if (!fi.exists())
|
|
return QStringLiteral("Элемент не существует: %1").arg(path);
|
|
if (fi.isDir()) {
|
|
QDir d(clean);
|
|
if (!d.removeRecursively())
|
|
return QStringLiteral("Не удалось удалить папку «%1».").arg(path);
|
|
QSet<QString> updated;
|
|
for (const QString &p : std::as_const(m_expanded)) {
|
|
if (p != clean && !p.startsWith(clean + '/'))
|
|
updated.insert(p);
|
|
}
|
|
m_expanded = std::move(updated);
|
|
} else {
|
|
if (!QFile::remove(clean))
|
|
return QStringLiteral("Не удалось удалить файл «%1».").arg(path);
|
|
}
|
|
rebuild();
|
|
return QString();
|
|
}
|
|
|
|
QString FileStore::moveItem(const QString &srcPath, const QString &destDir)
|
|
{
|
|
if (m_vault.isEmpty())
|
|
return QStringLiteral("Хранилище не открыто.");
|
|
if (srcPath.isEmpty() || destDir.isEmpty())
|
|
return QStringLiteral("Не указаны пути.");
|
|
const QString src = QDir::cleanPath(srcPath);
|
|
const QString dstDir = QDir::cleanPath(destDir);
|
|
if (!isInsideVault(src) || !isInsideVault(dstDir))
|
|
return QStringLiteral("Путь вне хранилища.");
|
|
QFileInfo srcInfo(src);
|
|
QFileInfo dstInfo(dstDir);
|
|
if (!srcInfo.exists())
|
|
return QStringLiteral("Источник не существует: %1").arg(src);
|
|
if (!dstInfo.exists() || !dstInfo.isDir())
|
|
return QStringLiteral("Папка назначения не существует или не является папкой: %1").arg(dstDir);
|
|
if (src == QDir::cleanPath(m_vault))
|
|
return QStringLiteral("Нельзя переместить корень хранилища.");
|
|
if (src == dstDir || dstDir == QDir::cleanPath(QFileInfo(src).absolutePath()))
|
|
return QString();
|
|
|
|
// Запрещаем перемещение папки в саму себя или в подпапку
|
|
if (dstDir == src || dstDir.startsWith(src + '/'))
|
|
return QStringLiteral("Нельзя переместить папку в саму себя.");
|
|
|
|
const QFileInfo fi(src);
|
|
const QString newPath = QDir(dstDir).filePath(fi.fileName());
|
|
const QString cleanNew = QDir::cleanPath(newPath);
|
|
if (!isInsideVault(cleanNew))
|
|
return QStringLiteral("Итоговый путь вне хранилища.");
|
|
if (QFileInfo::exists(newPath))
|
|
return QStringLiteral("Элемент с таким именем уже есть в папке назначения.");
|
|
|
|
bool ok = false;
|
|
if (fi.isDir())
|
|
ok = QDir().rename(src, newPath);
|
|
else
|
|
ok = QFile::rename(src, newPath);
|
|
if (!ok)
|
|
return QStringLiteral("Не удалось переместить «%1».").arg(src);
|
|
|
|
// переносим состояние раскрытия для перемещаемой папки и её подпапок
|
|
if (fi.isDir()) {
|
|
const QString oldAbs = src;
|
|
const QString newAbs = cleanNew;
|
|
QSet<QString> updated;
|
|
for (const QString &p : std::as_const(m_expanded)) {
|
|
if (p == oldAbs)
|
|
updated.insert(newAbs);
|
|
else if (p.startsWith(oldAbs + '/'))
|
|
updated.insert(newAbs + p.mid(oldAbs.size()));
|
|
else
|
|
updated.insert(p);
|
|
}
|
|
m_expanded = std::move(updated);
|
|
}
|
|
rebuild();
|
|
return QString();
|
|
}
|
|
|
|
QString FileStore::itemName(int row) const
|
|
{
|
|
if (row < 0 || row >= m_rows.size())
|
|
return QString();
|
|
return m_rows[row].name;
|
|
}
|
|
|
|
QString FileStore::itemPath(int row) const
|
|
{
|
|
if (row < 0 || row >= m_rows.size())
|
|
return QString();
|
|
return m_rows[row].path;
|
|
}
|
|
|
|
bool FileStore::itemIsDir(int row) const
|
|
{
|
|
return row >= 0 && row < m_rows.size() && m_rows[row].isDir;
|
|
}
|
|
|
|
bool FileStore::itemHasChildren(int row) const
|
|
{
|
|
return row >= 0 && row < m_rows.size() && m_rows[row].hasChildren;
|
|
}
|
|
|
|
bool FileStore::fileExists(const QString &path) const
|
|
{
|
|
return QFileInfo::exists(path);
|
|
} |