Click here to Skip to main content
15,868,096 members
Articles / Web Development / ASP.NET / ASP.NET Core

File Upload and Protected Downloads Handling In TypeScript and ASP.NET Core

Rate me:
Please Sign up or sign in to vote.
4.85/5 (4 votes)
7 Mar 2020CPOL3 min read 12.3K   8   2
File Handling with Axios, TypeScript, C# and ASP.NET Core
A short article showing a method for handling file uploads and downloads.

Introduction

I was recently working on web site where I needed to upload and download files. Seems this is a point of pain for some so I thought I'd write a little article (as it's been SO LONG since my last one it's well overdue) as much for my own posterity as much as anything else.

Background

The application I was building was a shop front for selling digital products. The front end is all written in VueJS with an ASP.NET Core backend API serving the files and SPA.

Using the code

The code is pretty self explanatory so I'll just include a brief synopsys of what each block is doing rather than going over everything in detail and muddying the topic.

Registering Your API

main.ts

Import your api module

JavaScript
import api from './services/api'; -- this is my sites api
...
Vue.prototype.$api = api;

api-plugin.d.ts

Attaches the $api variable to Vue and gives it a type of Api.

JavaScript
 import { Api } from './services/api';
 
 declare module 'vue/types/vue' {
     interface Vue {
         $api: Api;
     }
}

export default Api;

Uploading a File

In my VueJS component I created a variable inside the data() object for holding the files to be sent to the server.

JavaScript
files: new FormData(),

I added a handler method to respond to the user adding a file to the uploaded

JavaScript
handleFileUpload(fileList: any) {
    this.files.append('file', fileList[0], fileList[0].name);
},

The Vue component template contains the file input element

HTML
<input type="file" v-on:change="handleFileUpload($event.target.files)" />

Submitting The File

When the user then performs an action on your UI that triggers the uploads I call my API.

JavaScript
this.$api.uploadFile(this.files)
    .then((response: <<YourResponseType>>) => {
        this.hasError = false;
        this.message = 'File uploaded';
    }).catch((error) => {
        this.hasError = true;
        this.message = 'Error uploading file';
    });

The API Service Method

The component method shown above in tern calls this method on my API service.

JavaScript
public async uploadFile(fileData: FormData): Promise<<<YourResponseType>>> {
    return await axios.post('/api/to/your/upload/handler', fileData, { headers: { 'Content-Type': 'multipart/form-data' } })
        .then((response: any) => {
            return response.data;
        })
        .catch((error: any) => {
            throw new Error(error);
        });
}

ASP.NET Core API Method

The code within this method will vary greatly based on your own requirements but the basic structure will look something like this.

C#
[HttpPost("/api/to/your/upload/handler")]
[Consumes("multipart/form-data")]
public async Task<IActionResult> UploadHandler(IFormCollection uploads)
{
    if (uploads.Files.Length <= 0) { return BadRequest("No file data"); }

    foreach (var f in uploads.Files) 
    {
        var filePath = "YourGeneratedUniqueFilePath";
        using (var stream = System.IO.File.Create(filePath))
        {
            await file.CopyToAsync(stream);
        }
    }

    return Ok();
}

Downloading A File

Starting at the server side this time, your API method will look something like this. Since I was using Kestral I opted to use the ControllerBase.PhysicalFile() method but theres is also the base controller ControllerBase.File() return method on your controllers should that suit your needs better.

Since my uploads were associated with an entity in my data store, downloads were requested via an ID value but you could use any method that suits your needs.

C#
[HttpGet("[action]")]
public async Task<IActionResult> GetFile(string id)
{
    var dir = "GetOrBuildYourDirectoryString";
    var fileName = "GetYourFileName";

    var mimeType = GetMimeType(fileName);

    var path = Path.Combine(dir, fileName);

    return PhysicalFile(path, mimeType, version.FileName);
}

public string GetMimeType(string fileName)
{
    var provider = new FileExtensionContentTypeProvider();
    string contentType;
    if (!provider.TryGetContentType(fileName, out contentType))
    {
        contentType = "application/octet-stream";
    }

    return contentType;
}
Note: The FileExtensionContentTypeProvider type comes from the Microsoft.AspNetCore.StaticFiles NuGet package
Install-Package Microsoft.AspNetCore.StaticFiles -Version 2.2.0

Client Side API Download

In order to call this GetFile() method on the server our client side API service needs to expose a download method. This is where things can get a little tricky. You may have to configure your server to provide and/or expose the content disposition header. This is a little out of scope for this article as I want to remain concise to the topic.

I didn't need to perform any specific steps to access this header but I did have to perform a little jiggery pokery to extract the data I needed on the client side - chiefly the file name. This code isn't particularly nice unfortunately. If anyone has suggestions on how this might be imporved please let me know.

JavaScript
public async downloadFile(id: string): Promise<void> {
    return await axios.get('/api/download/getfile?id=' + id, { responseType : 'blob' } )
        .then((response: any) => {
            const disposition = response.headers['content-disposition'];
            let fileName = '';

            if (disposition && disposition.indexOf('attachment') !== -1) {
                const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
                const matches = filenameRegex.exec(disposition);
                if (matches != null && matches[1]) {
                    fileName = matches[1].replace(/['"]/g, '');
                }
            }

            const fileUrl = window.URL.createObjectURL(new Blob([response.data]));
            const fileLink = document.createElement('a');
            fileLink.href = fileUrl;
            fileLink.setAttribute('download', fileName);
            document.body.appendChild(fileLink);
            fileLink.click();
        })
        .catch((error: any) => {
            throw new Error(error);
        });
}

This will setup the client side download and open the local users normal browser file save dialog.

Protecting Uploaded Files

Given that the code above is "manually" handling file downloads as well as uploads it stands to reason that simple URLs to files within the browser HTML isn't the desired scenario. In my case the uploaded files were placed in a directory that I wanted to handle downloads for.

In order to protect this "downloads" directory I mapped a little bit of logic to the .NET Core IApplicationBuilder instance within the StartUp.cs Configure method. This intercepts any request to this URL and sends a 401 response.

C#
app.Map("/downloads", subApp => {
    subApp.Use(async (context, next) =>
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
    });
});

Anyone attempting to access a file within this downloads directory essentially gets booted out and the browser receives an error response for the server.

I hope you find something of use in this short descrption of an approach. Any feedback, suggestions or improvements are most welcome.

Thanks for reading.

History

V1.0 - 7th March 2020

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer JamSoft Solution Ltd
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionTwo things... Pin
Dewey15-Mar-20 7:51
Dewey15-Mar-20 7:51 
AnswerRe: Two things... Pin
Jammer19-Mar-20 12:03
Jammer19-Mar-20 12:03 
Yes, it's all fully working and tested in a web site project I'm working on.

The code is C# on the server (.NET Core) and the client side is VueJS and TypeScript. TypeScript is a bit C#ish. Maybe that's what you're thinking?
Jammer
My Blog | JamSoft

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.