From 3be4205276a49f68a93dadddd10ad37cf4fd2cb7 Mon Sep 17 00:00:00 2001
From: Rareshp <155653633+Rareshp@users.noreply.github.com>
Date: Sat, 24 Aug 2024 15:26:47 +0300
Subject: [PATCH 1/2] add import & export of xattr tags
---
package.json | 1 +
.../containers/Settings/ImportExport.tsx | 37 ++++
src/frontend/stores/FileStore.ts | 161 ++++++++++++++++++
yarn.lock | 5 +
4 files changed, 204 insertions(+)
diff --git a/package.json b/package.json
index abbf03da..34f3d855 100644
--- a/package.json
+++ b/package.json
@@ -140,6 +140,7 @@
"electron-google-analytics4": "^1.1.1",
"electron-updater": "^6.1.7",
"fs-extra": "^11.1.0",
+ "fs-xattr": "^0.4.0",
"geo-coordinates-parser": "^1.6.3",
"libheif-js": "^1.17.1",
"mapbox-gl": "^3.0.1",
diff --git a/src/frontend/containers/Settings/ImportExport.tsx b/src/frontend/containers/Settings/ImportExport.tsx
index 1ed47c7e..fec0e583 100644
--- a/src/frontend/containers/Settings/ImportExport.tsx
+++ b/src/frontend/containers/Settings/ImportExport.tsx
@@ -124,6 +124,43 @@ export const ImportExport = observer(() => {
+
+
+ Import tags from extended file attributes (KDE Dolphin tags).
+
+
+
+
+
+
Backup Database as File
diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts
index b58ce03a..25acf876 100644
--- a/src/frontend/stores/FileStore.ts
+++ b/src/frontend/stores/FileStore.ts
@@ -1,4 +1,5 @@
import fse from 'fs-extra';
+import { getAttribute, setAttribute } from 'fs-xattr'
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { getThumbnailPath } from 'common/fs';
@@ -128,6 +129,92 @@ class FileStore {
}
}
+ @action.bound async readExtendedAttributeTagsFromFiles(): Promise {
+ // NOTE: https://wiki.archlinux.org/title/Extended_attributes
+ // implemented KDE Dolphin tags and ratings
+ const toastKey = 'read-tags-from-file';
+ try {
+ const numFiles = this.fileList.length;
+ for (let i = 0; i < numFiles; i++) {
+ AppToaster.show(
+ {
+ message: `Reading tags from files ${((100 * i) / numFiles).toFixed(0)}%...`,
+ timeout: 0,
+ },
+ toastKey,
+ );
+ const file = runInAction(() => this.fileList[i]);
+
+ const absolutePath = file.absolutePath;
+
+ try {
+ // a buffer with comma separated strings
+ const kdeTags = await getAttribute(absolutePath, 'user.xdg.tags')
+ // balooScore is 5/5 stars, but is double in xfattr
+ const balooScore = await getAttribute(absolutePath, 'user.baloo.rating')
+ // convert buffer to string, then split in array. Also remove trailing whitespace
+ let tagsNameHierarchies = kdeTags.toString().split(',').filter(String)
+ tagsNameHierarchies.push('score:' + balooScore)
+
+ // Now that we know the tag names in file metadata, add them to the files in OneFolder
+
+ const { tagStore } = this.rootStore;
+ for (const tagHierarchy of tagsNameHierarchies) {
+ const match = tagStore.findByName(tagHierarchy[tagHierarchy.length - 1]);
+ if (match) {
+ // If there is a match to the leaf tag, just add it to the file
+ file.addTag(match);
+ } else {
+ // If there is no direct match to the leaf, insert it in the tag hierarchy: first check if any of its parents exist
+ // parent tags are written as: parentTag/subparent/subtag for example
+ if (tagHierarchy.includes('/')) {
+ let curTag = tagStore.root;
+ // further check for subparents
+ for (const nodeName of tagHierarchy.split('/')) {
+ const nodeMatch = tagStore.findByName(nodeName);
+ if (nodeMatch) {
+ curTag = nodeMatch;
+ } else {
+ curTag = await tagStore.create(curTag, nodeName);
+ }
+ }
+ file.addTag(curTag);
+ } else {
+ // base tag, not a parent tag
+ let curTag = tagStore.root;
+ const nodeMatch = tagStore.findByName(tagHierarchy);
+ if (nodeMatch) {
+ curTag = nodeMatch;
+ } else {
+ curTag = await tagStore.create(curTag, tagHierarchy);
+ }
+ file.addTag(curTag);
+ }
+ }
+ }
+ } catch (e) {
+ console.error('Could not import tags for', absolutePath, e);
+ }
+ }
+ AppToaster.show(
+ {
+ message: 'Reading tags from files... Done!',
+ timeout: 5000,
+ },
+ toastKey,
+ );
+ } catch (e) {
+ console.error('Could not read tags', e);
+ AppToaster.show(
+ {
+ message: 'Reading tags from files failed. Check the dev console for more details',
+ timeout: 5000,
+ },
+ toastKey,
+ );
+ }
+ }
+
// @action.bound async readFacesAnnotationsFromFiles(): Promise {
// const toastKey = 'read-faces-annotations-from-file';
// try {
@@ -233,6 +320,80 @@ class FileStore {
}
}
+ @action.bound async writeExtendedAttributeTagsToFiles(): Promise {
+ const toastKey = 'write-tags-to-file';
+ try {
+ const numFiles = this.fileList.length;
+ const tagFilePairs = runInAction(() =>
+ this.fileList.map((f) => ({
+ absolutePath: f.absolutePath,
+ tagHierarchy: Array.from(
+ f.tags,
+ action((t) => t.path),
+ ),
+ })),
+ );
+ let lastToastVal = '0';
+ for (let i = 0; i < tagFilePairs.length; i++) {
+ const newToastVal = ((100 * i) / numFiles).toFixed(0);
+ if (lastToastVal !== newToastVal) {
+ lastToastVal = newToastVal;
+ AppToaster.show(
+ {
+ message: `Writing tags to files ${newToastVal}%...`,
+ timeout: 0,
+ },
+ toastKey,
+ );
+ }
+
+ const { absolutePath, tagHierarchy } = tagFilePairs[i];
+ try {
+ let tagArray = []
+ let balooScore = '0'
+ for (const tagH of tagHierarchy) {
+ // readExtendedAttributeTagsFromFiles creates score:# for balooScore
+ // tagH is an array; even with one element
+ if (tagH[0].includes('score')) {
+ balooScore = tagH[0].split(':')[1]
+ // skipping adding it to tagArray
+ continue
+ }
+ if (typeof(tagH) != 'string') {
+ // concatenate parents with their sub elements
+ tagArray.push(tagH.join('/'))
+ } else {
+ tagArray.push(tagH)
+ }
+ };
+
+ // tagArray now must be joined into one string, comma separated
+ await setAttribute(absolutePath, 'user.xdg.tags', tagArray.join(','));
+ await setAttribute(absolutePath, 'user.baloo.rating', String(balooScore));
+
+ } catch (e) {
+ console.error('Could not write tags to', absolutePath, tagHierarchy, e);
+ }
+ }
+ AppToaster.show(
+ {
+ message: 'Writing tags to files... Done!',
+ timeout: 5000,
+ },
+ toastKey,
+ );
+ } catch (e) {
+ console.error('Could not write tags', e);
+ AppToaster.show(
+ {
+ message: 'Writing tags to files failed. Check the dev console for more details',
+ timeout: 5000,
+ },
+ toastKey,
+ );
+ }
+ }
+
@computed get showsAllContent(): boolean {
return this.content === Content.All;
}
diff --git a/yarn.lock b/yarn.lock
index 8cd9a62c..51132e98 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4403,6 +4403,11 @@ fs-minipass@^2.0.0:
dependencies:
minipass "^3.0.0"
+fs-xattr@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/fs-xattr/-/fs-xattr-0.4.0.tgz#30797399631287b740994a0bfab7822295e5f482"
+ integrity sha512-Lw90zx483YTGiHfR67IPtrbrZ+yBr1/W98v/iyTeSkUbixg/wrHfX5x9oMUOnirC5P7SZ5HlrpnIRMMqt8Ej0A==
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
From de0a49b35a91af7601470dcf7842c208a01da10c Mon Sep 17 00:00:00 2001
From: Rareshp <155653633+Rareshp@users.noreply.github.com>
Date: Sat, 24 Aug 2024 15:53:09 +0300
Subject: [PATCH 2/2] catch missing xattr fields
---
src/frontend/stores/FileStore.ts | 26 ++++++++++++++++++++------
1 file changed, 20 insertions(+), 6 deletions(-)
diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts
index 25acf876..213eb4ab 100644
--- a/src/frontend/stores/FileStore.ts
+++ b/src/frontend/stores/FileStore.ts
@@ -148,16 +148,30 @@ class FileStore {
const absolutePath = file.absolutePath;
try {
- // a buffer with comma separated strings
- const kdeTags = await getAttribute(absolutePath, 'user.xdg.tags')
- // balooScore is 5/5 stars, but is double in xfattr
- const balooScore = await getAttribute(absolutePath, 'user.baloo.rating')
+ // any of these fields may, or may not be present
+ let kdeTags = Buffer.from('')
+ let balooScore = Buffer.from('')
+ try {
+ // a buffer with comma separated strings
+ kdeTags = await getAttribute(absolutePath, 'user.xdg.tags')
+ } catch (e) {
+ console.error('Error reading user.xdg.tags for', absolutePath, e);
+ }
+ try {
+ // balooScore is 5/5 stars, but is double in xfattr
+ balooScore = await getAttribute(absolutePath, 'user.baloo.rating')
+ } catch (e) {
+ console.error('Error reading user.baloo.rating for', absolutePath, e);
+ }
+
// convert buffer to string, then split in array. Also remove trailing whitespace
let tagsNameHierarchies = kdeTags.toString().split(',').filter(String)
- tagsNameHierarchies.push('score:' + balooScore)
+ // if there is no score, skip adding a tag for it
+ if (balooScore.toString() != '') {
+ tagsNameHierarchies.push('score:' + balooScore.toString())
+ }
// Now that we know the tag names in file metadata, add them to the files in OneFolder
-
const { tagStore } = this.rootStore;
for (const tagHierarchy of tagsNameHierarchies) {
const match = tagStore.findByName(tagHierarchy[tagHierarchy.length - 1]);