Handle file uploads in Deno

Mayank C
Tech Tonic

--

Introduction

It’s quite common to have REST APIs that accepts file uploads. For example — uploaded images for product reviews, general images, documents, logs, etc.

Generally, such REST APIs handles uploaded files in three ways:

  • Process uploaded file: The uploaded file is processed without saving. Say run-time document processing, stats processing, aggregation, summarization, etc.
  • Save uploaded file: The uploaded file is directly saved on the disk (could be local or network mounted)
  • Forward uploaded file: The uploaded file is forwarded to another web service without processing. In this case, the HTTP server acts as an API proxy.

In this article, we’ll go over how to quickly write a file upload server in Deno that can take care of second and third cases, i.e. save uploaded file and forward uploaded file. The server will be native, i.e. without any frameworks.

If needed, an overview of ReadableStream in Deno is here.

Handling file upload

The basic logic in handling of file upload is as follows:

  • Deno’s native HTTP server receives HTTP request
  • The HTTP request is presented in the Request object
  • The Request object contains a ReadableStream
  • For the forward case, the ReadableStream can be directly forwarded in the fetch API call
  • For the other two cases,
  • A DefaultStreamReader is obtained from ReadableStream
  • The DefaultStreamReader is converted to Deno.Reader using standard library’s io module’s utility function readerFromStreamReader
  • Once Deno.Reader is available, it can be processed or directed to a file

In other words, for handling the file, conversion to Deno.Reader is preferable. For forwarding the file, there is no need to convert as the fetch API supports the Request interface.

Let’s go over the second and third case with example.

Save uploaded file

For this use-case, we’ll write an HTTP server that saves the uploaded file at a pre-defined path. The server tries to get file name from the query params. If not found, a random file name is generated. The received file is redirected to disk using Deno.copy.

import { readerFromStreamReader } from "https://deno.land/std/io/streams.ts";
import { serve } from "https://deno.land/std/http/mod.ts";
const SAVE_PATH = "./";async function reqHandler(req: Request) {
const url = new URL(req.url);
const fileName = url.searchParams.get("filename") || crypto.randomUUID();
if (!req.body) {
return new Response(null, { status: 400 });
}
const reader = req?.body?.getReader();
const f = await Deno.open(SAVE_PATH + fileName, {
create: true,
write: true,
});
await Deno.copy(readerFromStreamReader(reader), f);
await f.close();
return new Response();
}
serve(reqHandler, { port: 8000 });

Here is the output of some sample runs:

> curl --request POST --data-binary "@readings100M.txt" http://localhost:5000> ls -l
-rw-r--r-- 1 mayankc staff 104857600 Apr 6 18:26 readings100M.txt
-rw-r--r-- 1 mayankc staff 104857600 Jun 11 20:41 9fa80a15-1772-489c-bff5-42219070c2bc
> curl --request POST --data-binary "@readings100M.txt" http://localhost:5000?filename='uploadedReadings100M.txt'> ls -l
-rw-r--r-- 1 mayankc staff 104857600 Apr 6 18:26 readings100M.txt
-rw-r--r-- 1 mayankc staff 104857600 Jun 11 20:42 uploadedReadings100M.txt

Pass-through uploaded file

For this use-case, we’ll write an HTTP that forwards the uploaded file to another web service (aka proxying). The server again tries to get the file name from the query params. If the file name was received, it’d send it in the proxied request. The received ReadableStream (request.body) is given directly to fetch. No conversion or processing is required.

import { serve } from "https://deno.land/std/http/mod.ts";const proxyBaseUrl = "http://localhost:8100/";async function reqHandler(req: Request) {
const url = new URL(req.url);
const fileName = url.searchParams.get("filename") || crypto.randomUUID();
if (!req.body) {
return new Response(null, { status: 400 });
}
const proxyUrl = proxyBaseUrl + "?filename=" + fileName;
const headers = new Headers({
"content-length": req.headers.get("content-length") || "0",
});
const proxyRsp = await fetch(proxyUrl, {
method: "POST",
body: req.body,
headers,
});
if (proxyRsp.status !== 200) {
return new Response(null, { status: 500 });
}
await proxyRsp.text();
return new Response();
}
serve(reqHandler, { port: 8000 });

--

--