Skip to content

Instantly share code, notes, and snippets.

@domodomodomo
Last active May 30, 2024 18:58
Show Gist options
  • Select an option

  • Save domodomodomo/1589c9fa418538f08f0dd8b9fdbf282b to your computer and use it in GitHub Desktop.

Select an option

Save domodomodomo/1589c9fa418538f08f0dd8b9fdbf282b to your computer and use it in GitHub Desktop.
When a directory on the server is updated, notify the client's browser via WebSocket using watchdog.
"""
pip install fastapi uvicorn starlette watchdog
uvicorn detect_file_updates:app --reload
"""
import asyncio
import os
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from starlette.websockets import WebSocketState
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
TARGET_PATH = "/path/to/your/target/directory"
if not os.path.isdir(TARGET_PATH):
raise Exception("Target directory does not exist")
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Test</title>
</head>
<body>
<h1>WebSocket File Update Notification</h1>
<ul id="messages">
</ul>
<script>
const ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = function(event) {
const messages = document.getElementById('messages')
const message = document.createElement('li')
const content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
window.onbeforeunload = function() {
ws.close();
};
</script>
</body>
</html>
"""
class FileChangeEventHandler(FileSystemEventHandler):
def __init__(self, websocket):
self._websocket = websocket
self._event_loop = asyncio.get_event_loop()
def on_modified(self, event):
if event.is_directory:
return
#
# Point 1
#
# OK 1.
asyncio.run_coroutine_threadsafe(
self._websocket.send_text(event.src_path),
self._event_loop
)
# OK 2.
# self._event_loop.call_soon_threadsafe(
# self._event_loop.create_task,
# self._websocket.send_text(event.src_path)
# )
# NG
# asyncio.create_task(self._websocket.send_text(event.src_path))
#
# Point 2
#
print(event)
# > [!NOTE]
# > More than one event may occur for a single file change, like below.
# > Please ask ChatGPT for the reasons and countermeasures.
# ```
# INFO: connection open
# FileModifiedEvent(...)
# FileModifiedEvent(...)
# INFO: connection close
# ```
@app.get("/")
async def index():
return HTMLResponse(html)
@app.websocket("/ws")
async def ws(websocket: WebSocket):
await websocket.accept()
file_change_event_handler = FileChangeEventHandler(websocket)
observer = Observer()
observer.schedule(file_change_event_handler, TARGET_PATH, recursive=True)
observer.start()
try:
await websocket.receive_text()
except WebSocketDisconnect:
pass
finally:
observer.stop()
observer.join()
#
# Point 3
#
# To avoid closing it twice, I don't understand why it happens.
if websocket.client_state != WebSocketState.DISCONNECTED:
await websocket.close()
@domodomodomo
Copy link
Copy Markdown
Author

domodomodomo commented May 29, 2024

WebSocket handles events with functions, whereas watchdog handles events with methods defined in classes. This might make understanding the code a bit more difficult.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment