Existing settings are converted.
Works on most FS except FAT/exFAT which fallback to .directory files.
If the extended file attributes (in ADS in Windows) can't be saved, they are saved to file as before.
BUG: 322922
You can see file xattr using for instance for Unix filesystems:
getfattr -d /home/meven
KF6::WidgetsAddons
KF6::Codecs
KF6::KCMUtils
+ KF6::FileMetaData
${FTS_LIB}
)
if(HAVE_BALOO)
target_link_libraries(
dolphinprivate PUBLIC
- KF6::FileMetaData
KF6::Baloo
KF6::BalooWidgets
)
<entry name="ViewMode" type="Int" >
<label context="@label">View Mode</label>
<whatsthis context="@info:whatsthis">This option controls the style of the view. Currently supported values include icons (0), details (1) and column (2) views.</whatsthis>
- <default>DolphinView::IconsView</default>
+ <default code="true">DolphinView::IconsView</default>
</entry>
<entry name="PreviewsShown" type="Bool" >
globalViewPropsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_localViewProps = new QRadioButton(i18nc("@option:radio", "Remember display style for each folder"));
- QLabel *localViewPropsLabel = new QLabel(i18nc("@info", "Dolphin will create a hidden .directory file in each folder you change view properties for."));
+ QLabel *localViewPropsLabel = new QLabel(i18nc("@info",
+ "Dolphin will add file system metadata to folders you change view properties for. If that is not possible, "
+ "a hidden .directory file is created instead."));
localViewPropsLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
localViewPropsLabel->setWordWrap(true);
localViewPropsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
# ViewPropertiesTest
ecm_add_test(viewpropertiestest.cpp testdir.cpp
TEST_NAME viewpropertiestest
-LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test)
+LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test KF6::FileMetaData)
# DolphinMainWindowTest
ecm_add_test(dolphinmainwindowtest.cpp testdir.cpp ${CMAKE_SOURCE_DIR}/src/dolphin.qrc
#include "dolphin_generalsettings.h"
#include "testdir.h"
+#include <KFileMetaData/UserMetaData>
+
+#include <QStorageInfo>
#include <QTest>
class ViewPropertiesTest : public QObject
void cleanup();
void testReadOnlyBehavior();
+ void testReadOnlyDirectory();
void testAutoSave();
+ void testParamMigrationToFileAttr();
+ void testParamMigrationToFileAttrKeepDirectory();
+ void testExtendedAttributeFull();
private:
bool m_globalViewProps;
QVERIFY(!QFile::exists(dotDirectoryFile));
}
+void ViewPropertiesTest::testReadOnlyDirectory()
+{
+ auto localFolder = m_testDir->url().toLocalFile();
+ QString dotDirectoryFile = localFolder + "/.directory";
+ QVERIFY(!QFile::exists(dotDirectoryFile));
+
+ // restrict write permissions
+ QVERIFY(QFile(localFolder).setPermissions(QFileDevice::ReadOwner));
+
+ QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+ QVERIFY(props->isAutoSaveEnabled());
+ props->setSortRole("someNewSortRole");
+ props.reset();
+
+ const auto destinationDir = props->destinationDir(QStringLiteral("local")) + localFolder;
+ qDebug() << destinationDir;
+ QVERIFY(QDir(destinationDir).exists());
+
+ QVERIFY(!QFile::exists(dotDirectoryFile));
+ KFileMetaData::UserMetaData metadata(localFolder);
+ auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+ QVERIFY(viewProperties.isEmpty());
+
+ props.reset(new ViewProperties(m_testDir->url()));
+ QVERIFY(props->isAutoSaveEnabled());
+ QCOMPARE(props->sortRole(), "someNewSortRole");
+ props.reset();
+
+ metadata = KFileMetaData::UserMetaData(destinationDir);
+ if (metadata.isSupported()) {
+ QVERIFY(metadata.hasAttribute("kde.fm.viewproperties#1"));
+ } else {
+ QVERIFY(QFile::exists(destinationDir + "/.directory"));
+ }
+
+ // un-restrict write permissions
+ QFile(localFolder).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
+}
+
void ViewPropertiesTest::testAutoSave()
{
QString dotDirectoryFile = m_testDir->url().toLocalFile() + "/.directory";
props->setSortRole("someNewSortRole");
props.reset();
- QVERIFY(QFile::exists(dotDirectoryFile));
+ KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+ if (metadata.isSupported()) {
+ auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+ QVERIFY(!viewProperties.isEmpty());
+ QVERIFY(!QFile::exists(dotDirectoryFile));
+ } else {
+ QVERIFY(QFile::exists(dotDirectoryFile));
+ }
+}
+
+void ViewPropertiesTest::testParamMigrationToFileAttr()
+{
+ QString dotDirectoryFilePath = m_testDir->url().toLocalFile() + "/.directory";
+ QVERIFY(!QFile::exists(dotDirectoryFilePath));
+
+ const char *settingsContent = R"SETTINGS("
+[Dolphin]
+Version=4
+ViewMode=1
+Timestamp=2023,12,29,10,44,15.793
+VisibleRoles=text,CustomizedDetails,Details_text,Details_modificationtime,Details_type
+
+[Settings]
+HiddenFilesShown=true)SETTINGS";
+ auto dotDirectoryFile = QFile(dotDirectoryFilePath);
+ QVERIFY(dotDirectoryFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate));
+ QTextStream out(&dotDirectoryFile);
+ out << settingsContent;
+ dotDirectoryFile.close();
+
+ KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+ {
+ QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+ QCOMPARE(props->viewMode(), DolphinView::Mode::DetailsView);
+ QVERIFY(props->hiddenFilesShown());
+ props->save();
+
+ if (metadata.isSupported()) {
+ auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+ QVERIFY(!viewProperties.isEmpty());
+ QVERIFY(!QFile::exists(dotDirectoryFilePath));
+ } else {
+ QVERIFY(QFile::exists(dotDirectoryFilePath));
+ }
+ }
+
+ ViewProperties props(m_testDir->url());
+ QCOMPARE(props.viewMode(), DolphinView::Mode::DetailsView);
+ QVERIFY(props.hiddenFilesShown());
+}
+
+void ViewPropertiesTest::testParamMigrationToFileAttrKeepDirectory()
+{
+ QString dotDirectoryFilePath = m_testDir->url().toLocalFile() + "/.directory";
+ QVERIFY(!QFile::exists(dotDirectoryFilePath));
+
+ const char *settingsContent = R"SETTINGS("
+[Dolphin]
+Version=4
+ViewMode=1
+Timestamp=2023,12,29,10,44,15.793
+VisibleRoles=text,CustomizedDetails,Details_text,Details_modificationtime,Details_type
+
+[Settings]
+HiddenFilesShown=true
+
+[Other]
+ThoseShouldBeKept=true
+)SETTINGS";
+ auto dotDirectoryFile = QFile(dotDirectoryFilePath);
+ QVERIFY(dotDirectoryFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate));
+ QTextStream out(&dotDirectoryFile);
+ out << settingsContent;
+ dotDirectoryFile.close();
+
+ KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+ {
+ QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+ QCOMPARE(props->viewMode(), DolphinView::Mode::DetailsView);
+ QVERIFY(props->hiddenFilesShown());
+ props->save();
+
+ if (metadata.isSupported()) {
+ auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+ QVERIFY(!viewProperties.isEmpty());
+ }
+
+ QVERIFY(QFile::exists(dotDirectoryFilePath));
+ KConfig directorySettings(dotDirectoryFilePath, KConfig::SimpleConfig);
+ QCOMPARE(directorySettings.groupList(), {"Other"});
+ }
+
+ ViewProperties props(m_testDir->url());
+ QVERIFY(props.hiddenFilesShown());
+ QCOMPARE(props.viewMode(), DolphinView::Mode::DetailsView);
+
+ QVERIFY(QFile::exists(dotDirectoryFilePath));
+}
+
+void ViewPropertiesTest::testExtendedAttributeFull()
+{
+#ifndef Q_OS_UNIX
+ QSKIP("Only unix is supported, for this test");
+#endif
+ QString dotDirectoryFile = m_testDir->url().toLocalFile() + "/.directory";
+ QVERIFY(!QFile::exists(dotDirectoryFile));
+
+ KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+ if (!metadata.isSupported()) {
+ QSKIP("need extended attribute/filesystem metadata to be usefull");
+ }
+
+ QStorageInfo storageInfo(m_testDir->url().toLocalFile());
+ auto blockSize = storageInfo.blockSize();
+
+ KFileMetaData::UserMetaData::Error result;
+ // write a close to block size theorical maximum size for attributes in Linux for ext4
+ // and btrfs (4Kib typically) when ReiserFS/XFS allow XATTR_SIZE_MAX (64Kib)
+ result = metadata.setAttribute("data", QString(blockSize - 50, 'a'));
+ if (result != KFileMetaData::UserMetaData::NoSpace) {
+ QSKIP("File system supports metadata bigger than file system block size");
+ }
+
+ // write a close to 4k attribute, maximum size in Linux for ext4
+ // so next writing the file metadata fails
+ result = metadata.setAttribute("data", QString(blockSize - 60, 'a'));
+ QCOMPARE(result, KFileMetaData::UserMetaData::NoError);
+
+ QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+ QVERIFY(props->isAutoSaveEnabled());
+ props->setSortRole("someNewSortRole");
+ props.reset();
+
+ if (metadata.isSupported()) {
+ auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+ QVERIFY(viewProperties.isEmpty());
+ QVERIFY(QFile::exists(dotDirectoryFile));
+
+ QFile dotDirectory(dotDirectoryFile);
+ KConfig viewSettings(dotDirectoryFile, KConfig::SimpleConfig);
+ QCOMPARE(viewSettings.groupList(), {"Dolphin"});
+ QCOMPARE(viewSettings.group("Dolphin").readEntry("SortRole"), "someNewSortRole");
+ } else {
+ QVERIFY(QFile::exists(dotDirectoryFile));
+ }
}
QTEST_GUILESS_MAIN(ViewPropertiesTest)
ViewProperties props(viewPropertiesUrl());
props.setGroupedSorting(grouped);
- props.save();
- m_container->controller()->model()->setGroupedSorting(grouped);
+ m_model->setGroupedSorting(grouped);
Q_EMIT groupedSortingChanged(grouped);
}
#include "dolphindebug.h"
#include <QCryptographicHash>
+#include <QTemporaryFile>
#include <KFileItem>
+#include <KFileMetaData/UserMetaData>
namespace
{
const char ViewPropertiesFileName[] = ".directory";
}
+ViewPropertySettings *ViewProperties::loadProperties(const QString &folderPath) const
+{
+ const QString settingsFile = folderPath + QDir::separator() + ViewPropertiesFileName;
+
+ KFileMetaData::UserMetaData metadata(folderPath);
+ if (!metadata.isSupported()) {
+ return new ViewPropertySettings(KSharedConfig::openConfig(settingsFile, KConfig::SimpleConfig));
+ }
+
+ QTemporaryFile tempFile;
+ tempFile.setAutoRemove(false);
+ if (!tempFile.open()) {
+ qCWarning(DolphinDebug) << "Could not open temp file";
+ return nullptr;
+ }
+
+ if (QFile::exists(settingsFile)) {
+ // copy settings to tempfile to load them separately
+ QFile::remove(tempFile.fileName());
+ QFile::copy(settingsFile, tempFile.fileName());
+
+ auto config = KConfig(tempFile.fileName(), KConfig::SimpleConfig);
+ // ignore settings that are outside of dolphin scope
+ if (config.hasGroup("Dolphin") || config.hasGroup("Settings")) {
+ const auto groupList = config.groupList();
+ for (const auto &group : groupList) {
+ if (group != QStringLiteral("Dolphin") && group != QStringLiteral("Settings")) {
+ config.deleteGroup(group);
+ }
+ }
+ return new ViewPropertySettings(KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig));
+
+ } else if (!config.groupList().isEmpty()) {
+ // clear temp file content
+ QFile::remove(tempFile.fileName());
+ }
+ }
+
+ // load from metadata
+ const QString viewPropertiesString = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+ if (!viewPropertiesString.isEmpty()) {
+ // load view properties from xattr to temp file then loads into ViewPropertySettings
+ // clear the temp file
+ QFile outputFile(tempFile.fileName());
+ outputFile.open(QIODevice::WriteOnly);
+ outputFile.write(viewPropertiesString.toUtf8());
+ outputFile.close();
+ }
+ return new ViewPropertySettings(KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig));
+}
+
+ViewPropertySettings *ViewProperties::defaultProperties() const
+{
+ auto props = loadProperties(destinationDir(QStringLiteral("global")));
+ if (props == nullptr) {
+ qCWarning(DolphinDebug) << "Could not load default global viewproperties";
+ props = new ViewPropertySettings;
+ }
+
+ return props;
+}
+
ViewProperties::ViewProperties(const QUrl &url)
: m_changedProps(false)
, m_autoSave(true)
m_filePath = destinationDir(QStringLiteral("remote")) + m_filePath;
}
- const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
- m_node = new ViewPropertySettings(KSharedConfig::openConfig(file));
+ m_node = loadProperties(m_filePath);
+
+ bool useDefaultSettings = useGlobalViewProps ||
+ // If the props timestamp is too old,
+ // use default values instead.
+ (m_node != nullptr && (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
+ && m_node->timestamp() < settings->viewPropsTimestamp());
- // If the .directory file does not exist or the timestamp is too old,
- // use default values instead.
- const bool useDefaultProps = (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
- && (!QFile::exists(file) || (m_node->timestamp() < settings->viewPropsTimestamp()));
- if (useDefaultProps) {
+ if (m_node == nullptr) {
+ // no settings found for m_filepath, load defaults
+ m_node = defaultProperties();
+ useDefaultSettings = true;
+ }
+
+ // default values for special directories
+ if (useDefaultSettings) {
if (useSearchView) {
const QString path = url.path();
setSortRole(QByteArrayLiteral("modificationtime"));
}
} else {
- // The global view-properties act as default for directories without
- // any view-property configuration. Constructing a ViewProperties
- // instance for an empty QUrl ensures that the global view-properties
- // are loaded.
- QUrl emptyUrl;
- ViewProperties defaultProps(emptyUrl);
- setDirProperties(defaultProps);
-
m_changedProps = false;
}
}
save();
}
+ if (!m_node->config()->name().endsWith(ViewPropertiesFileName)) {
+ // remove temp file
+ QFile::remove(m_node->config()->name());
+ }
+
delete m_node;
m_node = nullptr;
}
void ViewProperties::save()
{
qCDebug(DolphinDebug) << "Saving view-properties to" << m_filePath;
+
+ auto cleanDotDirectoryFile = [this]() {
+ const QString settingsFile = m_filePath + QDir::separator() + ViewPropertiesFileName;
+ if (QFile::exists(settingsFile)) {
+ qCDebug(DolphinDebug) << "cleaning .directory" << settingsFile;
+ KConfig cfg(settingsFile, KConfig::OpenFlag::SimpleConfig);
+ const auto groupList = cfg.groupList();
+ for (const auto &group : groupList) {
+ if (group == QStringLiteral("Dolphin") || group == QStringLiteral("Settings")) {
+ cfg.deleteGroup(group);
+ }
+ }
+ if (cfg.groupList().isEmpty()) {
+ QFile::remove(settingsFile);
+ } else if (cfg.isDirty()) {
+ cfg.sync();
+ }
+ }
+ };
+
+ // ensures the destination dir exists, in case we don't write metadata directly on the folder
+ QDir destinationDir(m_filePath);
+ if (!destinationDir.exists() && !destinationDir.mkpath(m_filePath)) {
+ qCWarning(DolphinDebug) << "Could not create fake directory to store metadata";
+ }
+
+ KFileMetaData::UserMetaData metaData(m_filePath);
+ if (metaData.isSupported()) {
+ const auto metaDataKey = QStringLiteral("kde.fm.viewproperties#1");
+
+ const auto items = m_node->items();
+ const bool allDefault = std::all_of(items.cbegin(), items.cend(), [this](const KConfigSkeletonItem *item) {
+ return item->name() == "Timestamp" || (item->name() == "Version" && m_node->version() == CurrentViewPropertiesVersion) || item->isDefault();
+ });
+ if (allDefault) {
+ if (metaData.hasAttribute(metaDataKey)) {
+ qCDebug(DolphinDebug) << "clearing extended attributes for " << m_filePath;
+ const auto result = metaData.setAttribute(metaDataKey, QString());
+ if (result != KFileMetaData::UserMetaData::NoError) {
+ qCWarning(DolphinDebug) << "could not clear extended attributes for " << m_filePath << "error:" << result;
+ }
+ }
+ cleanDotDirectoryFile();
+ return;
+ }
+
+ // save config to disk
+ if (!m_node->save()) {
+ qCWarning(DolphinDebug) << "could not save viewproperties" << m_node->config()->name();
+ return;
+ }
+
+ QFile configFile(m_node->config()->name());
+ if (!configFile.open(QIODevice::ReadOnly)) {
+ qCWarning(DolphinDebug) << "Could not open readonly config file" << m_node->config()->name();
+ } else {
+ // load config from disk
+ const QString viewPropertiesString = configFile.readAll();
+
+ // save to xattr
+ const auto result = metaData.setAttribute(metaDataKey, viewPropertiesString);
+ if (result != KFileMetaData::UserMetaData::NoError) {
+ if (result == KFileMetaData::UserMetaData::NoSpace) {
+ // copy settings to dotDirectory file as fallback
+ if (!configFile.copy(m_filePath + QDir::separator() + ViewPropertiesFileName)) {
+ qCWarning(DolphinDebug) << "could not write viewproperties to .directory for dir " << m_filePath;
+ }
+ // free the space used by viewproperties from the file metadata
+ metaData.setAttribute(metaDataKey, "");
+ } else {
+ qCWarning(DolphinDebug) << "could not save viewproperties to extended attributes for dir " << m_filePath << "error:" << result;
+ }
+ // keep .directory file
+ return;
+ }
+ cleanDotDirectoryFile();
+ }
+
+ m_changedProps = false;
+ return;
+ }
+
QDir dir;
dir.mkpath(m_filePath);
m_node->setVersion(CurrentViewPropertiesVersion);
m_node->save();
- m_changedProps = false;
-}
-bool ViewProperties::exist() const
-{
- const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
- return QFile::exists(file);
+ m_changedProps = false;
}
QString ViewProperties::destinationDir(const QString &subDir) const
*/
void save();
- /**
- * @return True if properties for the given URL exist:
- * As soon as the properties for an URL have been saved with
- * ViewProperties::save(), true will be returned. If false is
- * returned, the default view-properties are used.
- */
- bool exist() const;
-
-private:
/**
* Returns the destination directory path where the view
* properties are stored. \a subDir specifies the used sub
*/
QString destinationDir(const QString &subDir) const;
+private:
/**
* Returns the view-mode prefix when storing additional properties for
* a view-mode.
*/
static QString directoryHashForUrl(const QUrl &url);
+ /** @returns a ViewPropertySettings object with properties loaded for the directory at @param filePath. Ownership is returned to the caller. */
+ ViewPropertySettings *loadProperties(const QString &folderPath) const;
+ /** @returns a ViewPropertySettings object with the globally configured default values. Ownership is returned to the caller. */
+ ViewPropertySettings *defaultProperties() const;
+
Q_DISABLE_COPY(ViewProperties)
private: