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.