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]);