mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-07-24 17:20:51 +02:00
refactor(fs): reduce overhead of watch (#2613)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
fs: minor
|
||||||
|
fs-js: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Reduce the overhead of `watch` and `unwatch`
|
||||||
Generated
-1
@@ -6599,7 +6599,6 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
"fs:allow-mkdir",
|
"fs:allow-mkdir",
|
||||||
"fs:allow-remove",
|
"fs:allow-remove",
|
||||||
"fs:allow-write-text-file",
|
"fs:allow-write-text-file",
|
||||||
|
"fs:read-meta",
|
||||||
"fs:scope-download-recursive",
|
"fs:scope-download-recursive",
|
||||||
"fs:scope-resource-recursive",
|
"fs:scope-resource-recursive",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,16 +2,18 @@
|
|||||||
import * as fs from "@tauri-apps/plugin-fs";
|
import * as fs from "@tauri-apps/plugin-fs";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
import { arrayBufferToBase64 } from "../lib/utils";
|
import { arrayBufferToBase64 } from "../lib/utils";
|
||||||
|
import { onDestroy } from "svelte";
|
||||||
|
|
||||||
export let onMessage;
|
export let onMessage;
|
||||||
export let insecureRenderHtml;
|
export let insecureRenderHtml;
|
||||||
|
|
||||||
let path = "";
|
let path = "";
|
||||||
let img;
|
let img;
|
||||||
|
/** @type {fs.FileHandle} */
|
||||||
let file;
|
let file;
|
||||||
let renameTo;
|
let renameTo;
|
||||||
let watchPath = "";
|
let watchPath = "";
|
||||||
let watchDebounceDelay = 0;
|
let watchDebounceDelay = "0";
|
||||||
let watchRecursive = false;
|
let watchRecursive = false;
|
||||||
let unwatchFn;
|
let unwatchFn;
|
||||||
let unwatchPath = "";
|
let unwatchPath = "";
|
||||||
@@ -118,7 +120,7 @@
|
|||||||
.getElementById("file-save")
|
.getElementById("file-save")
|
||||||
.addEventListener("click", function () {
|
.addEventListener("click", function () {
|
||||||
fs.writeTextFile(path, fileInput.value, {
|
fs.writeTextFile(path, fileInput.value, {
|
||||||
dir: getDir(),
|
baseDir: getDir(),
|
||||||
}).catch(onMessage);
|
}).catch(onMessage);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -170,6 +172,15 @@
|
|||||||
unwatchFn = undefined;
|
unwatchFn = undefined;
|
||||||
unwatchPath = undefined;
|
unwatchPath = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (file) {
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
if (unwatchFn) {
|
||||||
|
unwatchFn();
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ tauri = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
|
||||||
glob = { workspace = true }
|
glob = { workspace = true }
|
||||||
# TODO: Remove `serialization-compat-6` in v3
|
# TODO: Remove `serialization-compat-6` in v3
|
||||||
notify = { version = "8", optional = true, features = [
|
notify = { version = "8", optional = true, features = [
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ fn main() {
|
|||||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { metadata } from '@tauri-apps/plugin-fs'
|
import { stat } from '@tauri-apps/plugin-fs'
|
||||||
|
|
||||||
await metadata('/path/to/file')
|
await stat('/path/to/file')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -101,6 +101,7 @@ const COMMANDS: &[(&str, &[&str])] = &[
|
|||||||
("fstat", &[]),
|
("fstat", &[]),
|
||||||
("exists", &[]),
|
("exists", &[]),
|
||||||
("watch", &[]),
|
("watch", &[]),
|
||||||
|
// TODO: Remove this in v3
|
||||||
("unwatch", &[]),
|
("unwatch", &[]),
|
||||||
("size", &[]),
|
("size", &[]),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1245,15 +1245,44 @@ type WatchEventKindRemove =
|
|||||||
| { kind: 'folder' }
|
| { kind: 'folder' }
|
||||||
| { kind: 'other' }
|
| { kind: 'other' }
|
||||||
|
|
||||||
|
// TODO: Remove this in v3, return `Watcher` instead
|
||||||
/**
|
/**
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
type UnwatchFn = () => void
|
type UnwatchFn = () => void
|
||||||
|
|
||||||
async function unwatch(rid: number): Promise<void> {
|
class Watcher extends Resource {}
|
||||||
await invoke('plugin:fs|unwatch', { rid })
|
|
||||||
|
async function watchInternal(
|
||||||
|
paths: string | string[] | URL | URL[],
|
||||||
|
cb: (event: WatchEvent) => void,
|
||||||
|
options: DebouncedWatchOptions
|
||||||
|
): Promise<UnwatchFn> {
|
||||||
|
const watchPaths = Array.isArray(paths) ? paths : [paths]
|
||||||
|
|
||||||
|
for (const path of watchPaths) {
|
||||||
|
if (path instanceof URL && path.protocol !== 'file:') {
|
||||||
|
throw new TypeError('Must be a file URL.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEvent = new Channel<WatchEvent>()
|
||||||
|
onEvent.onmessage = cb
|
||||||
|
|
||||||
|
const rid: number = await invoke('plugin:fs|watch', {
|
||||||
|
paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)),
|
||||||
|
options,
|
||||||
|
onEvent
|
||||||
|
})
|
||||||
|
|
||||||
|
const watcher = new Watcher(rid)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void watcher.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Return `Watcher` instead in v3
|
||||||
/**
|
/**
|
||||||
* Watch changes (after a delay) on files or directories.
|
* Watch changes (after a delay) on files or directories.
|
||||||
*
|
*
|
||||||
@@ -1264,34 +1293,13 @@ async function watch(
|
|||||||
cb: (event: WatchEvent) => void,
|
cb: (event: WatchEvent) => void,
|
||||||
options?: DebouncedWatchOptions
|
options?: DebouncedWatchOptions
|
||||||
): Promise<UnwatchFn> {
|
): Promise<UnwatchFn> {
|
||||||
const opts = {
|
return await watchInternal(paths, cb, {
|
||||||
recursive: false,
|
|
||||||
delayMs: 2000,
|
delayMs: 2000,
|
||||||
...options
|
...options
|
||||||
}
|
|
||||||
|
|
||||||
const watchPaths = Array.isArray(paths) ? paths : [paths]
|
|
||||||
|
|
||||||
for (const path of watchPaths) {
|
|
||||||
if (path instanceof URL && path.protocol !== 'file:') {
|
|
||||||
throw new TypeError('Must be a file URL.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEvent = new Channel<WatchEvent>()
|
|
||||||
onEvent.onmessage = cb
|
|
||||||
|
|
||||||
const rid: number = await invoke('plugin:fs|watch', {
|
|
||||||
paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)),
|
|
||||||
options: opts,
|
|
||||||
onEvent
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
|
||||||
void unwatch(rid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Return `Watcher` instead in v3
|
||||||
/**
|
/**
|
||||||
* Watch changes on files or directories.
|
* Watch changes on files or directories.
|
||||||
*
|
*
|
||||||
@@ -1302,32 +1310,10 @@ async function watchImmediate(
|
|||||||
cb: (event: WatchEvent) => void,
|
cb: (event: WatchEvent) => void,
|
||||||
options?: WatchOptions
|
options?: WatchOptions
|
||||||
): Promise<UnwatchFn> {
|
): Promise<UnwatchFn> {
|
||||||
const opts = {
|
return await watchInternal(paths, cb, {
|
||||||
recursive: false,
|
|
||||||
...options,
|
...options,
|
||||||
delayMs: null
|
delayMs: undefined
|
||||||
}
|
|
||||||
|
|
||||||
const watchPaths = Array.isArray(paths) ? paths : [paths]
|
|
||||||
|
|
||||||
for (const path of watchPaths) {
|
|
||||||
if (path instanceof URL && path.protocol !== 'file:') {
|
|
||||||
throw new TypeError('Must be a file URL.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEvent = new Channel<WatchEvent>()
|
|
||||||
onEvent.onmessage = cb
|
|
||||||
|
|
||||||
const rid: number = await invoke('plugin:fs|watch', {
|
|
||||||
paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)),
|
|
||||||
options: opts,
|
|
||||||
onEvent
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
|
||||||
void unwatch(rid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -150,11 +150,6 @@ pub fn open<R: Runtime>(
|
|||||||
Ok(rid)
|
Ok(rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn close<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> CommandResult<()> {
|
|
||||||
webview.resources_table().close(rid).map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CopyFileOptions {
|
pub struct CopyFileOptions {
|
||||||
|
|||||||
@@ -397,7 +397,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
|||||||
commands::create,
|
commands::create,
|
||||||
commands::open,
|
commands::open,
|
||||||
commands::copy_file,
|
commands::copy_file,
|
||||||
commands::close,
|
|
||||||
commands::mkdir,
|
commands::mkdir,
|
||||||
commands::read_dir,
|
commands::read_dir,
|
||||||
commands::read,
|
commands::read,
|
||||||
@@ -420,8 +419,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
|||||||
commands::size,
|
commands::size,
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
watcher::watch,
|
watcher::watch,
|
||||||
#[cfg(feature = "watch")]
|
|
||||||
watcher::unwatch
|
|
||||||
])
|
])
|
||||||
.setup(|app, api| {
|
.setup(|app, api| {
|
||||||
let scope = Scope {
|
let scope = Scope {
|
||||||
|
|||||||
+43
-98
@@ -2,8 +2,8 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
|
use notify_debouncer_full::{new_debouncer, DebouncedEvent, Debouncer, RecommendedCache};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
ipc::{Channel, CommandScope, GlobalScope},
|
ipc::{Channel, CommandScope, GlobalScope},
|
||||||
@@ -11,15 +11,7 @@ use tauri::{
|
|||||||
Manager, Resource, ResourceId, Runtime, Webview,
|
Manager, Resource, ResourceId, Runtime, Webview,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::time::Duration;
|
||||||
path::PathBuf,
|
|
||||||
sync::{
|
|
||||||
mpsc::{channel, Receiver},
|
|
||||||
Mutex,
|
|
||||||
},
|
|
||||||
thread::spawn,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{resolve_path, CommandResult},
|
commands::{resolve_path, CommandResult},
|
||||||
@@ -27,79 +19,44 @@ use crate::{
|
|||||||
SafeFilePath,
|
SafeFilePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InnerWatcher {
|
#[allow(unused)]
|
||||||
pub kind: WatcherKind,
|
|
||||||
paths: Vec<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WatcherResource(Mutex<InnerWatcher>);
|
|
||||||
impl WatcherResource {
|
|
||||||
fn new(kind: WatcherKind, paths: Vec<PathBuf>) -> Self {
|
|
||||||
Self(Mutex::new(InnerWatcher { kind, paths }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_lock<R, F: FnMut(&mut InnerWatcher) -> R>(&self, mut f: F) -> R {
|
|
||||||
let mut watcher = self.0.lock().unwrap();
|
|
||||||
f(&mut watcher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resource for WatcherResource {}
|
|
||||||
|
|
||||||
enum WatcherKind {
|
enum WatcherKind {
|
||||||
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
|
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
|
||||||
Watcher(RecommendedWatcher),
|
Watcher(RecommendedWatcher),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch_raw(on_event: Channel<Event>, rx: Receiver<notify::Result<Event>>) {
|
impl Resource for WatcherKind {}
|
||||||
spawn(move || {
|
|
||||||
while let Ok(event) = rx.recv() {
|
|
||||||
if let Ok(event) = event {
|
|
||||||
// TODO: Should errors be emitted too?
|
|
||||||
let _ = on_event.send(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn watch_debounced(on_event: Channel<Event>, rx: Receiver<DebounceEventResult>) {
|
|
||||||
spawn(move || {
|
|
||||||
while let Ok(Ok(events)) = rx.recv() {
|
|
||||||
for event in events {
|
|
||||||
// TODO: Should errors be emitted too?
|
|
||||||
let _ = on_event.send(event.event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct WatchOptions {
|
pub struct WatchOptions {
|
||||||
base_dir: Option<BaseDirectory>,
|
base_dir: Option<BaseDirectory>,
|
||||||
|
#[serde(default)]
|
||||||
recursive: bool,
|
recursive: bool,
|
||||||
delay_ms: Option<u64>,
|
delay_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn watch<R: Runtime>(
|
pub fn watch<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
paths: Vec<SafeFilePath>,
|
paths: Vec<SafeFilePath>,
|
||||||
options: WatchOptions,
|
options: WatchOptions,
|
||||||
on_event: Channel<Event>,
|
on_event: Channel<notify::Event>,
|
||||||
global_scope: GlobalScope<Entry>,
|
global_scope: GlobalScope<Entry>,
|
||||||
command_scope: CommandScope<Entry>,
|
command_scope: CommandScope<Entry>,
|
||||||
) -> CommandResult<ResourceId> {
|
) -> CommandResult<ResourceId> {
|
||||||
let mut resolved_paths = Vec::with_capacity(paths.capacity());
|
let resolved_paths = paths
|
||||||
for path in paths {
|
.into_iter()
|
||||||
resolved_paths.push(resolve_path(
|
.map(|path| {
|
||||||
&webview,
|
resolve_path(
|
||||||
&global_scope,
|
&webview,
|
||||||
&command_scope,
|
&global_scope,
|
||||||
path,
|
&command_scope,
|
||||||
options.base_dir,
|
path,
|
||||||
)?);
|
options.base_dir,
|
||||||
}
|
)
|
||||||
|
})
|
||||||
|
.collect::<CommandResult<Vec<_>>>()?;
|
||||||
|
|
||||||
let recursive_mode = if options.recursive {
|
let recursive_mode = if options.recursive {
|
||||||
RecursiveMode::Recursive
|
RecursiveMode::Recursive
|
||||||
@@ -107,52 +64,40 @@ pub async fn watch<R: Runtime>(
|
|||||||
RecursiveMode::NonRecursive
|
RecursiveMode::NonRecursive
|
||||||
};
|
};
|
||||||
|
|
||||||
let kind = if let Some(delay) = options.delay_ms {
|
let watcher_kind = if let Some(delay) = options.delay_ms {
|
||||||
let (tx, rx) = channel();
|
let mut debouncer = new_debouncer(
|
||||||
let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?;
|
Duration::from_millis(delay),
|
||||||
|
None,
|
||||||
|
move |events: Result<Vec<DebouncedEvent>, Vec<notify::Error>>| {
|
||||||
|
if let Ok(events) = events {
|
||||||
|
for event in events {
|
||||||
|
// TODO: Should errors be emitted too?
|
||||||
|
let _ = on_event.send(event.event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
for path in &resolved_paths {
|
for path in &resolved_paths {
|
||||||
debouncer.watch(path, recursive_mode)?;
|
debouncer.watch(path, recursive_mode)?;
|
||||||
}
|
}
|
||||||
watch_debounced(on_event, rx);
|
|
||||||
WatcherKind::Debouncer(debouncer)
|
WatcherKind::Debouncer(debouncer)
|
||||||
} else {
|
} else {
|
||||||
let (tx, rx) = channel();
|
let mut watcher = RecommendedWatcher::new(
|
||||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
move |event| {
|
||||||
|
if let Ok(event) = event {
|
||||||
|
// TODO: Should errors be emitted too?
|
||||||
|
let _ = on_event.send(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)?;
|
||||||
for path in &resolved_paths {
|
for path in &resolved_paths {
|
||||||
watcher.watch(path, recursive_mode)?;
|
watcher.watch(path, recursive_mode)?;
|
||||||
}
|
}
|
||||||
watch_raw(on_event, rx);
|
|
||||||
WatcherKind::Watcher(watcher)
|
WatcherKind::Watcher(watcher)
|
||||||
};
|
};
|
||||||
|
|
||||||
let rid = webview
|
let rid = webview.resources_table().add(watcher_kind);
|
||||||
.resources_table()
|
|
||||||
.add(WatcherResource::new(kind, resolved_paths));
|
|
||||||
|
|
||||||
Ok(rid)
|
Ok(rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn unwatch<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> CommandResult<()> {
|
|
||||||
let watcher = webview.resources_table().take::<WatcherResource>(rid)?;
|
|
||||||
WatcherResource::with_lock(&watcher, |watcher| {
|
|
||||||
match &mut watcher.kind {
|
|
||||||
WatcherKind::Debouncer(ref mut debouncer) => {
|
|
||||||
for path in &watcher.paths {
|
|
||||||
debouncer.unwatch(path).map_err(|e| {
|
|
||||||
format!("failed to unwatch path: {} with error: {e}", path.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WatcherKind::Watcher(ref mut w) => {
|
|
||||||
for path in &watcher.paths {
|
|
||||||
w.unwatch(path).map_err(|e| {
|
|
||||||
format!("failed to unwatch path: {} with error: {e}", path.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user