Skip to content

Instantly share code, notes, and snippets.

@domodomodomo
Last active May 30, 2024 18:58
Show Gist options
  • Save domodomodomo/1589c9fa418538f08f0dd8b9fdbf282b to your computer and use it in GitHub Desktop.
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
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