Real-time file processing with deno and websockets
With the rise of real-time applications, developers are constantly seeking efficient ways to handle live data streams. Deno, a modern runtime for JavaScript and TypeScript, offers robust support for WebSockets and introduces enhanced security features, making it an excellent choice for building secure and efficient real-time file processing applications.
In this DevTip, we'll explore how to leverage Deno's WebSocket capabilities and its secure permissions model to implement a real-time file processing server. We'll cover setting up a WebSocket server, securely handling file uploads in real-time, processing files asynchronously, and broadcasting results back to clients.
Why deno?
Deno is a secure runtime for JavaScript and TypeScript that promises a more streamlined and safe development experience. Unlike Node.js, Deno includes TypeScript support out of the box and has a built-in permissions system to enhance security. It provides a sandboxed environment where scripts have no file system, network, or environment access unless explicitly enabled, which helps in mitigating security risks.
Setting up a deno websocket server
First, let's set up a simple WebSocket server using Deno. Create a new file called server.ts
:
import { serve } from 'https://deno.land/std@0.200.0/http/mod.ts'
async function handleWs(sock: WebSocket) {
console.log('WebSocket connection established')
for await (const ev of sock) {
if (typeof ev === 'string') {
console.log('Received message:', ev)
// Handle incoming messages
}
}
}
console.log('Starting WebSocket server on :8080')
serve(
async (req) => {
if (req.headers.get('upgrade') !== 'websocket') {
return new Response('WebSocket server', { status: 200 })
}
const { socket, response } = Deno.upgradeWebSocket(req)
handleWs(socket)
return response
},
{ port: 8080 },
)
This code sets up a basic WebSocket server that listens on port 8080. It accepts incoming WebSocket connections and handles incoming messages.
Running the server
To run the server, execute:
deno run --allow-net server.ts
The --allow-net
flag grants the script network access, which is required for the server to accept
connections.
Handling real-time file uploads securely
We can modify our WebSocket handler to accept file data sent from clients. Let's update the
handleWs
function:
async function handleWs(sock: WebSocket) {
console.log('WebSocket connection established')
for await (const ev of sock) {
if (typeof ev === 'string') {
const message = JSON.parse(ev)
if (message.type === 'file') {
const { filename, data } = message
console.log(`Received file: ${filename}`)
// Decode the base64 file data
const fileData = Uint8Array.from(atob(data), (c) => c.charCodeAt(0))
// Save the file temporarily
await Deno.writeFile(`./uploads/${filename}`, fileData)
// Process the file asynchronously
processFile(filename, sock)
}
}
}
}
In this code, we expect the client to send a JSON string containing the file's name and data encoded
in base64. We decode the data and save it to the uploads
directory.
Implementing file permissions
Deno's permissions model allows us to specify exactly what our script is allowed to do. To ensure
secure file operations, we need to explicitly grant file system access. When running the script,
include the --allow-read
and --allow-write
flags with specific paths:
deno run --allow-net --allow-read=./uploads --allow-write=./uploads server.ts
By specifying ./uploads
, we limit the script's read and write permissions to the uploads
directory, enhancing security.
Processing files asynchronously
Let's implement the processFile
function to handle file processing:
async function processFile(filename: string, sock: WebSocket) {
console.log(`Processing file: ${filename}`)
// Read the file content
const filePath = `./uploads/${filename}`
const fileData = await Deno.readFile(filePath)
// Perform a simple processing task (e.g., calculating file size)
const fileSize = fileData.length
console.log(`File size: ${fileSize} bytes`)
// Simulate processing delay
await new Promise((resolve) => setTimeout(resolve, 2000))
// Notify the client that processing is complete
const message = {
type: 'processed',
filename,
status: 'completed',
fileSize,
}
sock.send(JSON.stringify(message))
}
In this function, we read the file from the uploads
directory and perform a simple
operation—calculating the file size. In a real application, you might perform tasks like image
resizing, format conversion, or content scanning.
Broadcasting results to clients
After processing, we send a message back to the client over the WebSocket connection to notify them of the completion.
// Within processFile function
sock.send(JSON.stringify(message))
This real-time feedback loop provides users with immediate updates, enhancing the responsiveness of your application.
Client-side implementation
To test our server, we'll create a simple client using HTML and JavaScript. Create a file called
client.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>WebSocket File Upload</title>
</head>
<body>
<h1>WebSocket File Upload</h1>
<input type="file" id="fileInput" />
<script>
const sock = new WebSocket('ws://localhost:8080')
sock.addEventListener('open', () => {
console.log('Connected to the server')
})
sock.addEventListener('message', (event) => {
const message = JSON.parse(event.data)
if (message.type === 'processed') {
alert(
`File ${message.filename} processed successfully. File size: ${message.fileSize} bytes.`,
)
}
})
const fileInput = document.getElementById('fileInput')
fileInput.addEventListener('change', () => {
const file = fileInput.files[0]
const reader = new FileReader()
reader.onload = () => {
const data = btoa(reader.result)
const message = {
type: 'file',
filename: file.name,
data,
}
sock.send(JSON.stringify(message))
}
reader.readAsBinaryString(file)
})
</script>
</body>
</html>
This client allows users to select a file, which is then sent to the server over a WebSocket connection. The server processes the file and notifies the client upon completion.
Best practices and common pitfalls
Utilizing deno permissions for safe operations
Deno's permission system is one of its standout features, enhancing security by requiring explicit permission flags. Always specify the minimal permissions needed for your application. For example:
deno run --allow-net --allow-read=./uploads --allow-write=./uploads server.ts
By restricting read and write permissions to specific directories, you prevent unauthorized access to other parts of the file system.
Managing error handling
Include robust error handling in your applications. Ensure that you handle cases where the client disconnects unexpectedly or sends malformed data. For example:
try {
const message = JSON.parse(ev)
// Proceed with handling the message
} catch (error) {
console.error('Error parsing message:', error)
sock.send(JSON.stringify({ type: 'error', message: 'Invalid data format' }))
}
Preventing common mistakes
Be cautious of resource usage, especially when handling file uploads and processing. Implement measures to prevent resource exhaustion, such as:
- Limiting file sizes
- Restricting the number of concurrent connections
- Validating file types before processing
Secure file handling
Always validate and sanitize file inputs to prevent security vulnerabilities like path traversal attacks. For example, ensure filenames do not contain unexpected characters:
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.\-_]/g, '')
const filePath = `./uploads/${sanitizedFilename}`
Conclusion
Deno's built-in support for WebSockets and its secure permissions model make it an excellent choice for building secure and efficient real-time file processing applications. By leveraging WebSockets and Deno's permission system, we can create responsive applications that provide immediate feedback to users while maintaining robust security.
While building such applications, always be mindful of the security implications of file handling and ensure you're utilizing Deno's features to safeguard your operations.
At Transloadit, we appreciate the efficiency and security that Deno offers in file processing workflows. Feel free to explore our File Filtering service for advanced file handling capabilities.