diff --git a/package.json b/package.json index b0afc68d..ad7e69d5 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,7 @@ { "name": "allusion", "productName": "Allusion", - "version": "1.0.0-ruc.8.0", - "description": "A tool for managing your visual library", "main": "build/main.bundle.js", "scripts": { @@ -68,6 +66,17 @@ }, "files": [ "!node_modules", + "node_modules/@parcel/**/*", + "node_modules/micromatch/**/*", + "node_modules/braces/**/*", + "node_modules/fill-range/**/*", + "node_modules/to-regex-range/**/*", + "node_modules/is-number/**/*", + "node_modules/is-glob/**/*", + "node_modules/detect-libc/**/*", + "node_modules/picomatch/**/*", + "node_modules/node-addon-api/**/*", + "node_modules/is-extglob/**/*", "build/**/*", "package.json" ], @@ -124,8 +133,8 @@ "dependencies": { "@floating-ui/core": "^1.2.1", "@floating-ui/react-dom": "^1.3.0", + "@parcel/watcher": "^2.5.1", "ag-psd": "^15.0.0", - "chokidar": "^3.5.3", "comlink": "^4.4.1", "dexie": "^3.2.3", "dexie-export-import": "^1.0.3", @@ -144,4 +153,4 @@ "utif": "^3.1.0", "wasm-feature-detect": "^1.2.11" } -} \ No newline at end of file +} diff --git a/src/frontend/workers/folderWatcher.worker.ts b/src/frontend/workers/folderWatcher.worker.ts index cd7d278d..ba8c8fac 100644 --- a/src/frontend/workers/folderWatcher.worker.ts +++ b/src/frontend/workers/folderWatcher.worker.ts @@ -1,16 +1,14 @@ -import chokidar, { FSWatcher } from 'chokidar'; import { expose } from 'comlink'; -import { Stats } from 'fs'; -import { BigIntStats } from 'original-fs'; +import { statSync } from 'fs'; import SysPath from 'path'; -import { RECURSIVE_DIR_WATCH_DEPTH } from 'common/config'; import { IMG_EXTENSIONS_TYPE } from 'src/api/file'; import { FileStats } from '../stores/LocationStore'; +import * as parcelWatcher from '@parcel/watcher'; const ctx: Worker = self as any; export class FolderWatcherWorker { - private watcher?: FSWatcher; + private watcher?: parcelWatcher.AsyncSubscription; // Whether the initial scan has been completed, and new/removed files are being watched private isReady = false; private isCancelled = false; @@ -20,7 +18,7 @@ export class FolderWatcherWorker { } async close() { - await this.watcher?.close(); + this.watcher?.unsubscribe(); } /** Returns all supported image files in the given directly, and callbacks for new or removed files */ @@ -33,68 +31,31 @@ export class FolderWatcherWorker { // Watch for files being added/changed/removed: // Usually you'd include a glob in the watch argument, e.g. `directory/**/.{jpg|png|...}`, but we cannot use globs unfortunately (see disableGlobbing) - // Instead, we ignore everything but image files in the `ignored` option - this.watcher = chokidar.watch(directory, { - disableGlobbing: true, // needed in order to support directories with brackets, quotes, asterisks, etc. - alwaysStat: true, // we need stats anyways during importing - depth: RECURSIVE_DIR_WATCH_DEPTH, // not really needed: added as a safety measure for infinite recursion between symbolic links - ignored: (path: string, stats?: Stats) => { - // We used to set `ignored` with regex patterns, but ran into problem with directories that - // contain dots in their file name. - // We ignore everything except image files but chokidar also matches entire directories. If - // those contain a dot, they will be ignored since they don't end with an image extension. - // So now we have to use a callback function that also provides `stats` through which we can - // use to detect whether the path is a file or a directory. - - const basename = SysPath.basename(path); - - // Ignore .dot files and folders. - if (basename.startsWith('.')) { - return true; - } - // If the path doesn't have an extension (likely a directory), don't ignore it. - // In the unlikely situation it is a file, we'll filter it out later in the .on('add', ...) - const ext = SysPath.extname(path).toLowerCase().split('.')[1]; - if (!ext) { - return false; - } - // If the path (file or directory) ends with an image extension, don't ignore it. - if (extensions.includes(ext as IMG_EXTENSIONS_TYPE)) { - return false; - } - // Otherwise, we need to know whether it is a file or a directory before making a decision. - // If we don't return anything, this callback will be called a second time, with the stats - // variable as second argument - if (stats) { - // Ignore if - // * dot directory like `/home/.hidden-directory/` but not `/home/directory.with.dots/` and - // * not a directory, and not an image file either. - return !stats.isDirectory() || SysPath.basename(path).startsWith('.'); - } - return false; - }, - }); - - const watcher = this.watcher; - - // Make a list of all files in this directory, which will be returned when all subdirs have been traversed - const initialFiles: FileStats[] = []; - - return new Promise((resolve) => { - watcher - // we can assume stats exist since we passed alwaysStat: true to chokidar - .on('add', async (path, stats: Stats | BigIntStats) => { - if (this.isCancelled) { - console.log('Cancelling file watching'); - await watcher.close(); - resolve(undefined); - this.isCancelled = false; + // watch for this https://github.com/parcel-bundler/watcher/pull/207 + this.isReady = true; + + this.watcher = await parcelWatcher.subscribe( + directory, + (err, events) => { + for (const event of events) { + if (err) { + console.error('Error fired in watcher', directory, err); + ctx.postMessage({ type: 'error', value: err }); } - - const ext = SysPath.extname(path).toLowerCase().split('.')[1]; - if (extensions.includes(ext as IMG_EXTENSIONS_TYPE)) { + // Ignore Files that aren't our extension type + const ext = SysPath.extname(event.path).toLowerCase().split('.')[1]; + if (!extensions.includes(ext as IMG_EXTENSIONS_TYPE)) { + continue; + } + if (event.type === 'create') { + const stats = statSync(event.path); + if (this.isCancelled) { + console.log('Cancelling file watching'); + this.watcher?.unsubscribe(); + this.isCancelled = false; + } /** - * Chokidar doesn't detect renames as a unique event, it detects a "remove" and "add" event. + * Chokidar and @parcel/watcher doesn't detect renames as a unique event, it detects a "remove" and "add" event. * We use the "ino" field of file stats to detect whether a new file is a previously detected file that was moved/renamed * Relevant issue https://github.com/paulmillr/chokidar/issues/303#issuecomment-127039892 * Inspiration for using "ino" from https://github.com/chrismaltby/gb-studio/pull/576 @@ -104,7 +65,7 @@ export class FolderWatcherWorker { */ const fileStats: FileStats = { - absolutePath: path, + absolutePath: event.path, dateCreated: stats.birthtime, dateModified: stats.mtime, size: Number(stats.size), @@ -116,41 +77,39 @@ export class FolderWatcherWorker { } else { initialFiles.push(fileStats); } + } else if (event.type === 'update') { + const stats = statSync(event.path); + if (this.isCancelled) { + console.log('Cancelling file watching'); + this.watcher?.unsubscribe(); + this.isCancelled = false; + } + const ext = SysPath.extname(event.path).toLowerCase().split('.')[1]; + if (extensions.includes(ext as IMG_EXTENSIONS_TYPE)) { + const fileStats: FileStats = { + absolutePath: event.path, + dateCreated: stats.birthtime, + dateModified: stats.mtime, + size: Number(stats.size), + ino: stats.ino.toString(), + }; + ctx.postMessage({ type: 'update', value: fileStats }); + } + } else if (event.type === 'delete') { + ctx.postMessage({ type: 'remove', value: event.path }); } - }) - .on('change', async (path, stats: Stats | BigIntStats) => { - if (this.isCancelled) { - console.log('Cancelling file watching'); - await watcher.close(); - this.isCancelled = false; - } - const ext = SysPath.extname(path).toLowerCase().split('.')[1]; - if (extensions.includes(ext as IMG_EXTENSIONS_TYPE)) { - const fileStats: FileStats = { - absolutePath: path, - dateCreated: stats.birthtime, - dateModified: stats.mtime, - size: Number(stats.size), - ino: stats.ino.toString(), - }; - ctx.postMessage({ type: 'update', value: fileStats }); - } - }) - // TODO: on directory change: update location hierarchy list - .on('unlink', (path: string) => ctx.postMessage({ type: 'remove', value: path })) - .on('ready', () => { - this.isReady = true; - resolve(initialFiles); + } + }, + { ignore: [] }, + ); + + // Make a list of all files in this directory, which will be returned when all subdirs have been traversed + const initialFiles: FileStats[] = []; - // Clear memory: initialFiles no longer needed - // Doing this immediately after resolving will resolve with an empty list for some reason - // So, do it with a timeout. Would be nicer to do it after an acknowledgement from the main thread - setTimeout(() => initialFiles.splice(0, initialFiles.length), 5000); - }) - .on('error', (error) => { - console.error('Error fired in watcher', directory, error); - ctx.postMessage({ type: 'error', value: error }); - }); + // This is stubbed out as @parcel/watcher doesn't have a ready event like chokidar + // Because @parcel/watcher has the ability to have snapshots and historical changes, we can use it to reduce startup time + return new Promise((resolve) => { + resolve([]); }); } } diff --git a/webpack.dev.js b/webpack.dev.js index efcabf70..7aa69783 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -43,7 +43,8 @@ let mainConfig = { ], }, externals: { - fsevents: "require('fsevents')" + fsevents: "require('fsevents')", + "@parcel/watcher": "require('@parcel/watcher')" } }; @@ -123,7 +124,8 @@ let rendererConfig = { }), ], externals: { - fsevents: "require('fsevents')" + fsevents: "require('fsevents')", + "@parcel/watcher": "require('@parcel/watcher')" } }; diff --git a/webpack.prod.js b/webpack.prod.js index 3e21af49..9522da84 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -41,6 +41,9 @@ let mainConfig = { }, ], }, + externals: { + "@parcel/watcher": "require('@parcel/watcher')" + } }; let rendererConfig = { @@ -132,6 +135,9 @@ let rendererConfig = { chunkFilename: '[id].[contenthash].css', }), ], + externals: { + "@parcel/watcher": "require('@parcel/watcher')" + } }; module.exports = [mainConfig, rendererConfig]; diff --git a/yarn.lock b/yarn.lock index e2f3e68d..3451d63f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1401,6 +1401,95 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + +"@parcel/watcher@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" + "@sinclair/typebox@^0.25.16": version "0.25.24" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" @@ -2472,6 +2561,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.4: version "4.21.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" @@ -2644,7 +2740,7 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: +"chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -2988,6 +3084,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -3669,6 +3770,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -5089,6 +5197,14 @@ micromatch@^4.0.0, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -5227,6 +5343,11 @@ node-addon-api@^1.6.3: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-exiftool@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/node-exiftool/-/node-exiftool-2.3.0.tgz#d5142d34de6f1683b4655198b648e7e3ee6e80ac"