Building a Simple File Upload App with Angel
Google’s Dart language is awesome. After using JavaScript for about a year, I felt suffocated by its chaotic environment and endless flood of dependencies. Dart is not only immune to this problem, but also as a language contains many notable improvements over JavaScript. Dart is excellent on the client-side, but on the server-side, there is no framework that really sticks out to me. Thus, the Angel Framework was born.
Angel is an easily-extensible framework inspired by Express, FeathersJS and Laravel. It has several plugins available, a ready-to-go boilerplate and even a client library. The framework is still in development, but we can get a taste of it by making a small application.
Let’s build a simple server that accepts file uploads and spits out information about uploaded files.
The easiest way to start an Angel project is by installing the Angel CLI. Run the following in your Terminal or Command Prompt, assuming you already have the Dart SDK installed:
pub global activate angel_cli
Now, let’s initialize a new project. Run:
angel init uploads
This will create a new project called uploads and install all required dependencies.
You will notice that your project already consists of several libraries. The one we will be modifying here is located in lib/src/routes/routes.dart, and, as you can imagine, contains our application routes. Find the method configureRoutes, and remove the route mounted on “/”. The code should now look like this:
app.all('*', serveStatic());
This will send all requests through our static file server middleware, which serves files from our web directory (or build/web in production). Next, let’s create a file at web/index.html. The exact contents don’t matter, but make sure you have a form that POSTs to /upload and sends data as multipart/form-data. This form should contain a file input. Example:
<!DOCTYPE html>
<html>
<head>
<title>File Information</title>
</head>
<body>
<h1>Upload a file to see information.</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input name="file" type="file" />
<br /><br />
<input type="submit" value="Submit" />
</form>
</body>
</html>
The user interface doesn’t need to be complicated, as the focus of this mini-project is the server side.
Now, head to lib/src/routes/routes.dart. Right before the code that is already there, let’s add some code to process an uploaded file and spit out some information about it. In addition, if the user uploads an image, we should display it. For now, our handler should look a bit like this:
app.post("/upload", (RequestContext req, res) async {
if (req.files.isEmpty || req.files[0].data.isEmpty)
throw new AngelHttpException.BadRequest(
message: "Please upload a file. :)");
var file = req.files[0];
}
Taking a look at the code, it is very easy to figure out what it does:
- If the user has not uploaded a file, or has uploaded an empty one, the server throws a 400 Bad Request error with the message “Please upload a file. :)”. If we had requested JSON, this error would be serialized JSON. Otherwise, it will be passed through our registered error handler, which in most cases is a function to render an error page.
- Otherwise, create a reference to the first file the user uploaded. As you can infer, Angel supports the upload of multiple files.
Once we have a reference to this file, let’s do the following:
- Print its filename
- Display it on-screen if it is an image. The easiest way to do this is to Base64-encode it. This is trivial in Dart, if we import the dart:convert library.
- Print size in kilobytes
- Print MIME type
- Print number of lines
Perhaps declare a few constants, and create a helper method:
final List<String> _IMAGE_EXTENSIONS = <String>[
"jpg",
"jpeg",
"png",
"gif",
"tiff",
"ico"
];final int _NEWLINE = "\n".codeUnits.first;bool _isImage(file) {
var ext = file.filename.split(".").last;
return _IMAGE_EXTENSIONS.contains(ext);
}
Our route handler could possibly look like this now:
app.post("/upload", (RequestContext req, res) async {
if (req.files.isEmpty || req.files[0].data.isEmpty)
throw new AngelHttpException.BadRequest(
message: "Please upload a file. :)");
var file = req.files[0];
var nLines = file.data.where((n) => n == _NEWLINE).length; res
..header("Content-Type", "text/html")
..write('''
<!DOCTYPE html>
<html>
<head>
<title>${file.filename}</title>
</head>
<body>
<h1>${file.filename}</h1>''');
if (_isImage(file)) {
var base64String = BASE64.encode(file.data);
res.write('<img src="data:${file.mimeType};base64,$base64String" />');
}
res
..write('''
<table>
<tr>
<td><b>Size:</b></td>
<td>${file.data.length / 1000}kb</td>
</tr>
<tr>
<td><b>MIME Type:</b></td>
<td>${file.mimeType}</td>
</tr>
<tr>
<td><b>Number of Lines:</b></td>
<td>${nLines + 1}</td>
</tr>
</table>
</body>
</html>
''')
..end();
});
Fortunately for us, most of our applications will be SPA’s, so it is rare we will have to include HTML strings in our server code. Besides, there is a Mustache view generator plugin available for Angel, and is installed by default with the boilerplate.
Finally, we can try out our app. It’s not the prettiest, but it does work.
Congratulations! You’ve just created your first Angel application. Give yourself a pat on the back.
Check out the source code for this example.
With Angular2 or another SPA framework, you can build amazing full-stack apps, especially when combined with a powerful framework like Angel. The framework will keep growing and getting better until it is a strong enough factor to persuade people to leave buggy systems of the past and migrate to Dart. However, for now, it is still in development. Expect a 1.0.0 release by late 2016, or early 2017.
You’ve probably noticed just by looking at the boilerplate that there is much more to the Angel framework than just simple file upload services. Go ahead and explore it for yourself. Feedback is greatly appreciated, as the library is far from perfect.