Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add recursive directory watch #488

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add watch dir recursive option (missing python client regeneration)
  • Loading branch information
dobrac committed Dec 19, 2024
commit cbb8927c4d30d78129955d285be502272c726831
12 changes: 11 additions & 1 deletion packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file filesystem/filesystem.proto.
*/
export const file_filesystem_filesystem: GenFile = /*@__PURE__*/
fileDesc("ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSIeCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHwoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiRAoPRmlsZXN5c3RlbUV2ZW50EgwKBG5hbWUYASABKAkSIwoEdHlwZRgCIAEoDjIVLmZpbGVzeXN0ZW0uRXZlbnRUeXBlIuABChBXYXRjaERpclJlc3BvbnNlEjgKBXN0YXJ0GAEgASgLMicuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLlN0YXJ0RXZlbnRIABIxCgpmaWxlc3lzdGVtGAIgASgLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnRIABI7CglrZWVwYWxpdmUYAyABKAsyJi5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UuS2VlcEFsaXZlSAAaDAoKU3RhcnRFdmVudBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiJAoUQ3JlYXRlV2F0Y2hlclJlcXVlc3QSDAoEcGF0aBgBIAEoCSIrChVDcmVhdGVXYXRjaGVyUmVzcG9uc2USEgoKd2F0Y2hlcl9pZBgBIAEoCSItChdHZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIkcKGEdldFdhdGNoZXJFdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5maWxlc3lzdGVtLkZpbGVzeXN0ZW1FdmVudCIqChRSZW1vdmVXYXRjaGVyUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIhcKFVJlbW92ZVdhdGNoZXJSZXNwb25zZSpSCghGaWxlVHlwZRIZChVGSUxFX1RZUEVfVU5TUEVDSUZJRUQQABISCg5GSUxFX1RZUEVfRklMRRABEhcKE0ZJTEVfVFlQRV9ESVJFQ1RPUlkQAiqYAQoJRXZlbnRUeXBlEhoKFkVWRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIVChFFVkVOVF9UWVBFX0NSRUFURRABEhQKEEVWRU5UX1RZUEVfV1JJVEUQAhIVChFFVkVOVF9UWVBFX1JFTU9WRRADEhUKEUVWRU5UX1RZUEVfUkVOQU1FEAQSFAoQRVZFTlRfVFlQRV9DSE1PRBAFMp8FCgpGaWxlc3lzdGVtEjkKBFN0YXQSFy5maWxlc3lzdGVtLlN0YXRSZXF1ZXN0GhguZmlsZXN5c3RlbS5TdGF0UmVzcG9uc2USQgoHTWFrZURpchIaLmZpbGVzeXN0ZW0uTWFrZURpclJlcXVlc3QaGy5maWxlc3lzdGVtLk1ha2VEaXJSZXNwb25zZRI5CgRNb3ZlEhcuZmlsZXN5c3RlbS5Nb3ZlUmVxdWVzdBoYLmZpbGVzeXN0ZW0uTW92ZVJlc3BvbnNlEkIKB0xpc3REaXISGi5maWxlc3lzdGVtLkxpc3REaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5MaXN0RGlyUmVzcG9uc2USPwoGUmVtb3ZlEhkuZmlsZXN5c3RlbS5SZW1vdmVSZXF1ZXN0GhouZmlsZXN5c3RlbS5SZW1vdmVSZXNwb25zZRJHCghXYXRjaERpchIbLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXF1ZXN0GhwuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlMAESVAoNQ3JlYXRlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXNwb25zZRJdChBHZXRXYXRjaGVyRXZlbnRzEiMuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBokLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlElQKDVJlbW92ZVdhdGNoZXISIC5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVzcG9uc2VCaQoOY29tLmZpbGVzeXN0ZW1CD0ZpbGVzeXN0ZW1Qcm90b1ABogIDRlhYqgIKRmlsZXN5c3RlbcoCCkZpbGVzeXN0ZW3iAhZGaWxlc3lzdGVtXEdQQk1ldGFkYXRh6gIKRmlsZXN5c3RlbWIGcHJvdG8z");
fileDesc("ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSIeCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iMgoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIkQKD0ZpbGVzeXN0ZW1FdmVudBIMCgRuYW1lGAEgASgJEiMKBHR5cGUYAiABKA4yFS5maWxlc3lzdGVtLkV2ZW50VHlwZSLgAQoQV2F0Y2hEaXJSZXNwb25zZRI4CgVzdGFydBgBIAEoCzInLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5TdGFydEV2ZW50SAASMQoKZmlsZXN5c3RlbRgCIAEoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50SAASOwoJa2VlcGFsaXZlGAMgASgLMiYuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLktlZXBBbGl2ZUgAGgwKClN0YXJ0RXZlbnQaCwoJS2VlcEFsaXZlQgcKBWV2ZW50IjcKFENyZWF0ZVdhdGNoZXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIisKFUNyZWF0ZVdhdGNoZXJSZXNwb25zZRISCgp3YXRjaGVyX2lkGAEgASgJIi0KF0dldFdhdGNoZXJFdmVudHNSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiRwoYR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlEisKBmV2ZW50cxgBIAMoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50IioKFFJlbW92ZVdhdGNoZXJSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiFwoVUmVtb3ZlV2F0Y2hlclJlc3BvbnNlKlIKCEZpbGVUeXBlEhkKFUZJTEVfVFlQRV9VTlNQRUNJRklFRBAAEhIKDkZJTEVfVFlQRV9GSUxFEAESFwoTRklMRV9UWVBFX0RJUkVDVE9SWRACKpgBCglFdmVudFR5cGUSGgoWRVZFTlRfVFlQRV9VTlNQRUNJRklFRBAAEhUKEUVWRU5UX1RZUEVfQ1JFQVRFEAESFAoQRVZFTlRfVFlQRV9XUklURRACEhUKEUVWRU5UX1RZUEVfUkVNT1ZFEAMSFQoRRVZFTlRfVFlQRV9SRU5BTUUQBBIUChBFVkVOVF9UWVBFX0NITU9EEAUynwUKCkZpbGVzeXN0ZW0SOQoEU3RhdBIXLmZpbGVzeXN0ZW0uU3RhdFJlcXVlc3QaGC5maWxlc3lzdGVtLlN0YXRSZXNwb25zZRJCCgdNYWtlRGlyEhouZmlsZXN5c3RlbS5NYWtlRGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTWFrZURpclJlc3BvbnNlEjkKBE1vdmUSFy5maWxlc3lzdGVtLk1vdmVSZXF1ZXN0GhguZmlsZXN5c3RlbS5Nb3ZlUmVzcG9uc2USQgoHTGlzdERpchIaLmZpbGVzeXN0ZW0uTGlzdERpclJlcXVlc3QaGy5maWxlc3lzdGVtLkxpc3REaXJSZXNwb25zZRI/CgZSZW1vdmUSGS5maWxlc3lzdGVtLlJlbW92ZVJlcXVlc3QaGi5maWxlc3lzdGVtLlJlbW92ZVJlc3BvbnNlEkcKCFdhdGNoRGlyEhsuZmlsZXN5c3RlbS5XYXRjaERpclJlcXVlc3QaHC5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UwARJUCg1DcmVhdGVXYXRjaGVyEiAuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEl0KEEdldFdhdGNoZXJFdmVudHMSIy5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXF1ZXN0GiQuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USVAoNUmVtb3ZlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXNwb25zZUJpCg5jb20uZmlsZXN5c3RlbUIPRmlsZXN5c3RlbVByb3RvUAGiAgNGWFiqAgpGaWxlc3lzdGVtygIKRmlsZXN5c3RlbeICFkZpbGVzeXN0ZW1cR1BCTWV0YWRhdGHqAgpGaWxlc3lzdGVtYgZwcm90bzM");

/**
* @generated from message filesystem.MoveRequest
Expand Down Expand Up @@ -218,6 218,11 @@ export type WatchDirRequest = Message<"filesystem.WatchDirRequest"> & {
* @generated from field: string path = 1;
*/
path: string;

/**
* @generated from field: bool recursive = 2;
*/
recursive: boolean;
};

/**
Expand Down Expand Up @@ -318,6 323,11 @@ export type CreateWatcherRequest = Message<"filesystem.CreateWatcherRequest"> &
* @generated from field: string path = 1;
*/
path: string;

/**
* @generated from field: bool recursive = 2;
*/
recursive: boolean;
};

/**
Expand Down
9 changes: 8 additions & 1 deletion packages/js-sdk/src/sandbox/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 90,10 @@ export interface WatchOpts extends FilesystemRequestOpts {
* Callback to call when the watch operation stops.
*/
onExit?: (err?: Error) => void | Promise<void>
/**
* Watch the directory recursively
*/
recursive?: boolean
}

/**
Expand Down Expand Up @@ -446,7 450,10 @@ export class Filesystem {
: undefined

const events = this.rpc.watchDir(
{ path },
{
path,
recursive: opts?.recursive ?? false,
},
{
headers: {
...authenticationHeader(opts?.user),
Expand Down
65 changes: 65 additions & 0 deletions packages/js-sdk/tests/sandbox/files/watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 31,71 @@ sandboxTest('watch directory changes', async ({ sandbox }) => {
await handle.stop()
})

sandboxTest('watch recursive directory changes', async ({ sandbox }) => {
const dirname = 'test_watch_dir'
const nestedDirname = 'test_nested_watch_dir'
const filename = 'test_watch.txt'
const content = 'This file will be watched.'
const newContent = 'This file has been modified.'

await sandbox.files.remove(dirname)
await sandbox.files.makeDir(`${dirname}/${nestedDirname}`)
await sandbox.files.write(`${dirname}/${nestedDirname}/${filename}`, content)

let trigger: () => void

const eventPromise = new Promise<void>((resolve) => {
trigger = resolve
})

const expectedFileName = `${nestedDirname}/${filename}`
const handle = await sandbox.files.watchDir(dirname, async (event) => {
if (event.type === FilesystemEventType.WRITE && event.name === expectedFileName) {
trigger()
}
}, {
recursive: true
})

await sandbox.files.write(`${dirname}/${nestedDirname}/${filename}`, newContent)

await eventPromise

await handle.stop()
})

sandboxTest('watch recursive directory folder addition', async ({ sandbox }) => {
const dirname = 'test_watch_dir'
const nestedDirname = 'test_nested_watch_dir'
const filename = 'test_watch.txt'
const content = 'This file will be watched.'

await sandbox.files.remove(dirname)
await sandbox.files.makeDir(dirname)

let trigger: () => void

const eventPromise = new Promise<void>((resolve) => {
trigger = resolve
})

const expectedFileName = `${nestedDirname}/${filename}`
const handle = await sandbox.files.watchDir(dirname, async (event) => {
if (event.type === FilesystemEventType.WRITE && event.name === expectedFileName) {
trigger()
}
}, {
recursive: true
})

await sandbox.files.makeDir(`${dirname}/${nestedDirname}`)
await sandbox.files.write(`${dirname}/${nestedDirname}/${filename}`, content)
dobrac marked this conversation as resolved.
Show resolved Hide resolved

await eventPromise

await handle.stop()
})
dobrac marked this conversation as resolved.
Show resolved Hide resolved

sandboxTest('watch non-existing directory', async ({ sandbox }) => {
const dirname = 'non_existing_watch_dir'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 343,7 @@ async def watch_dir(
user: Username = "user",
request_timeout: Optional[float] = None,
timeout: Optional[float] = 60,
recursive: bool = False,
) -> AsyncWatchHandle:
"""
Watch directory for filesystem events.
Expand All @@ -353,11 354,12 @@ async def watch_dir(
:param user: Run the operation as this user
:param request_timeout: Timeout for the request in **seconds**
:param timeout: Timeout for the watch operation in **seconds**. Using `0` will not limit the watch time
:param recursive: Watch directory recursively

:return: `AsyncWatchHandle` object for stopping watching directory
"""
events = self._rpc.awatch_dir(
filesystem_pb2.WatchDirRequest(path=path),
filesystem_pb2.WatchDirRequest(path=path, recursive=recursive),
request_timeout=self._connection_config.get_request_timeout(
request_timeout
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,19 336,21 @@ def watch_dir(
path: str,
user: Username = "user",
request_timeout: Optional[float] = None,
recursive: bool = False,
) -> WatchHandle:
"""
Watch directory for filesystem events.

:param path: Path to a directory to watch
:param user: Run the operation as this user
:param request_timeout: Timeout for the request in **seconds**
:param recursive: Watch directory recursively

:return: `WatchHandle` object for stopping watching directory
"""
try:
r = self._rpc.create_watcher(
filesystem_pb2.CreateWatcherRequest(path=path),
filesystem_pb2.CreateWatcherRequest(path=path, recursive=recursive),
request_timeout=self._connection_config.get_request_timeout(
request_timeout
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 35,57 @@ def handle_event(e: FilesystemEvent):
await handle.stop()


async def test_watch_recursive_directory_changes(async_sandbox: AsyncSandbox):
dirname = "test_watch_dir"
nested_dirname = "test_nested_watch_dir"
filename = "test_watch.txt"
content = "This file will be watched."

await async_sandbox.files.remove(dirname)
await async_sandbox.files.make_dir(f"{dirname}/{nested_dirname}")

event_triggered = Event()

expected_filename = f"{nested_dirname}/{filename}"
def handle_event(e: FilesystemEvent):
if e.type == FilesystemEventType.WRITE and e.name == expected_filename:
event_triggered.set()

handle = await async_sandbox.files.watch_dir(dirname, on_event=handle_event, recursive=True)

await async_sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)

await event_triggered.wait()

await handle.stop()


async def test_watch_recursive_directory_folder_addition(async_sandbox: AsyncSandbox):
dirname = "test_watch_dir"
nested_dirname = "test_nested_watch_dir"
filename = "test_watch.txt"
content = "This file will be watched."

await async_sandbox.files.remove(dirname)
await async_sandbox.files.make_dir(dirname)

event_triggered = Event()

expected_filename = f"{nested_dirname}/{filename}"
def handle_event(e: FilesystemEvent):
if e.type == FilesystemEventType.WRITE and e.name == expected_filename:
event_triggered.set()

handle = await async_sandbox.files.watch_dir(dirname, on_event=handle_event, recursive=True)

await async_sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
await async_sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)

await event_triggered.wait()

await handle.stop()


async def test_watch_non_existing_directory(async_sandbox: AsyncSandbox):
dirname = "non_existing_watch_dir"

Expand Down
51 changes: 51 additions & 0 deletions packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 8,9 @@ def test_watch_directory_changes(sandbox: Sandbox):
filename = "test_watch.txt"
content = "This file will be watched."

sandbox.files.remove(dirname)
sandbox.files.make_dir(dirname)

handle = sandbox.files.watch_dir(dirname)
sandbox.files.write(f"{dirname}/{filename}", content)

Expand Down Expand Up @@ -46,6 48,55 @@ def test_watch_iterated(sandbox: Sandbox):
handle.stop()


def test_watch_recursive_directory_changes(sandbox: Sandbox):
dirname = "test_watch_dir"
nested_dirname = "test_nested_watch_dir"
filename = "test_watch.txt"
content = "This file will be watched."

sandbox.files.remove(dirname)
sandbox.files.make_dir(f"{dirname}/{nested_dirname}")

handle = sandbox.files.watch_dir(dirname, recursive=True)
sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)

events = handle.get_new_events()
assert len(events) == 3
expected_filename = f"{nested_dirname}/{filename}"
assert events[0].type == FilesystemEventType.CREATE
assert events[0].name == expected_filename

handle.stop()


def test_watch_recursive_directory_folder_addition(sandbox: Sandbox):
dirname = "test_watch_dir"
nested_dirname = "test_nested_watch_dir"
filename = "test_watch.txt"
content = "This file will be watched."

sandbox.files.remove(dirname)
sandbox.files.make_dir(dirname)

handle = sandbox.files.watch_dir(dirname, recursive=True)

sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)

expected_filename = f"{nested_dirname}/{filename}"

events = handle.get_new_events()
file_changed = False
for event in events:
if event.type == FilesystemEventType.WRITE and event.name == expected_filename:
file_changed = True
break

assert file_changed

handle.stop()


def test_watch_non_existing_directory(sandbox: Sandbox):
dirname = "non_existing_watch_dir"

Expand Down
4 changes: 3 additions & 1 deletion spec/envd/filesystem/filesystem.proto
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 70,7 @@ message ListDirResponse {

message WatchDirRequest {
string path = 1;
bool recursive = 2;
}

message FilesystemEvent {
Expand All @@ -91,6 92,7 @@ message WatchDirResponse {

message CreateWatcherRequest {
string path = 1;
bool recursive = 2;
}

message CreateWatcherResponse {
Expand All @@ -106,7 108,7 @@ message GetWatcherEventsResponse {
}

message RemoveWatcherRequest {
string watcher_id = 1;
string watcher_id = 1;
}

message RemoveWatcherResponse {}
Expand Down