Download Uploaded Files Are Broken in Angular
- Create an Angular application
- Set up server-side rendering with Angular Universal and Node.js
- Implement file transfer operations
- Create a dynamic, in-memory list of files
- Pass data about storage content between the server awarding and the JavaScript used past Angular for SSR
- Node.js and npm (The Node.js installation will also install npm.)
- Athwart CLI
- Git
- Working cognition of TypeScript and the Angular framework
- Familiarity with Athwart observables and dependency injection
- Some exposure to Node.js
- The user performs a Get / request to the server.
- Node.js receives the asking.
- Node.js launches Angular and renders the view on the server.
- Data is stored in the TransferState registry.
- The server-side rendered view, including the browser-side JavaScript and TransferState registry, is passed to the browser and the Angular application is re-rendered in the browser.
- The browser sends a Go / request to the server.
- Server fires Athwart to render the view and calls the
constructor()
of theFileService
. - The constructor uses the
isPlatformServer()
method to make up one's mind if information technology is existence executed in Node.js on the server. If and then, the constructor calls thelistFiles()
method injected intoFileService
as a callback. ThelistFiles()
method provides the current listing of the contents of the user_upload directory, which is then stored in thefileList
local variable. - The listing of files is stored in the
TransferState
object. - The rendered view is ship back to the browser and the browser displays the view and bootstraps Angular on the client.
- The client calls the
constructor()
again and usesisPlatformServer()
to determine that the code is being executed on the client. - The
constructor()
retrieves list of files from theTransferState
object.
How to Transfer Files and Data Between Angular Clients and Node.js Backends
Having a shared codebase for both the server-side and browser-side code of an Angular application aids the maintainability of a project. Yous tin can do that with Angular Universal and Node.js using the server-side rendering (SSR) concept. You lot can even use SSR to securely pass data, including files, between the application server (Node.js) and the Angular awarding running on information technology.
This postal service will show yous how to create an application for uploading, storing, managing, and downloading files from a Node.js server using a single codebase. When yous finish this project you'll be able to:
To attain the tasks in this mail you volition need the following:
These tools are referred to in the instructions, but are non required:
To learn nearly finer from this post you lot should accept the post-obit:
You can learn more than about server-side rendering (SSR) in a previous Twilio blog post.
At that place is a companion repository for this post available on GitHub.
Create the project and components and service files
In this step y'all will implement a showtime "typhoon" of the application. You will create a form which will exist used for uploading files to the server and y'all will create an in-memory list of uploaded files. Every bit always, you need to offset by initializing the project.
Go to the directory under which yous'd like to create the projection and enter the following command line instructions to initialize the project and add Athwart Forms:
ng new athwart-and-nodejs-data --fashion css --routing imitation cd angular-and-nodejs-data/ npm install @angular/forms
Execute the following control line teaching to create the FileService
grade:
Execute the following commands to create the FileUploaderComponent
and FileListComponent
classes:
ng k c fileUploader --skipTests ng grand c fileList --skipTests
Be certain to carefully note the casing of the component names.
Create the file service
The initial implementation of the FileService
will be a temporary one that will enable users to add together and remove files from a list, but it won't actually move the files anywhere. It connects the file list and file uploader components and maintains the file list just, as you tin can meet from the code below, it doesn't have upload or download functionality.
Replace the contents of the src/app/file.service.ts file with the following TypeScript code:
import { Injectable } from '@angular/core'; import { BehaviorSubject, Subject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) consign grade FileService { private fileList: string[] = new Array<string>(); private fileList$: Subject<string[]> = new Discipline<string[]>(); constructor() { } public upload(fileName: string, fileContent: string): void { this.fileList.push(fileName); this.fileList$.next(this.fileList); } public download(fileName: string): void { } public remove(fileName): void { this.fileList.splice(this.fileList.findIndex(name => name === fileName), ane); this.fileList$.next(this.fileList); } public list(): Observable<string[]> { return this.fileList$; } private addFileToList(fileName: string): void { this.fileList.push(fileName); this.fileList$.next(this.fileList); } }
Create the file uploader component
The user interface for the file uploader component will exist based on a course, and then information technology's necessary to import the ReactiveFormsModule
into the main Angular AppModule
.
Add the post-obit import statement to the src/app/app.module.ts file:
import { ReactiveFormsModule } from '@athwart/forms';
Modify the imports:
section of the src/app/app.module.ts file to include ReactiveFormsModule
:
imports: [ BrowserModule, ReactiveFormsModule ],
Implementation of the FileUploaderComponent
begins with a template the user can use to choose a file to upload.
Supplant the contents of the src/app/file-uploader/file-uploader.component.html file with the following HTML markup:
<h1>Upload file</h1> <form [formGroup] = "formGroup" (ngSubmit)="onSubmit()"> <input blazon="file" (change)="onFileChange($event)" /> <input type="submit" [disabled]="formGroup.invalid" value="upload" /> </form>
Implement the logic for uploading files in the FileUploaderComponent
form.
Replace the contents of the src/app/file-uploader/file-uploader.component.ts file with the following TypeScript code:
import { Component, OnInit } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { FileService } from '../file.service'; @Component({ selector: 'app-file-uploader', templateUrl: './file-uploader.component.html', styleUrls: ['./file-uploader.component.css'] }) export class FileUploaderComponent { public formGroup = this.fb.group({ file: [null, Validators.required] }); private fileName; constructor(private fb: FormBuilder, individual fileService: FileService) { } public onFileChange(upshot) { const reader = new FileReader(); if (event.target.files && event.target.files.length) { this.fileName = event.target.files[0].name; const [file] = outcome.target.files; reader.readAsDataURL(file); reader.onload = () => { this.formGroup.patchValue({ file: reader.outcome }); }; } } public onSubmit(): void { this.fileService.upload(this.fileName, this.formGroup.go('file').value); } }
Note that the onFileChange()
method is jump to the (change)
action of the input type="file"
element of the HTML form. Also note that the patchValue
method of the formGroup
object is used to provide Angular with the contents of reader
so it tin can go on with the validation of the form.
When the grade is submitted the onSubmit()
event fires and uploads the named file to fileService
, where the file list is updated.
Create the file list component
The FileListComponent
class implements methods for retrieving list of files from the FileService
. It also provides download and remove operations that can be performed on the listed files.
Supervene upon the contents of the src/app/file-list/file-list.component.ts file with the following TypeScript lawmaking:
import { Component, OnInit } from '@angular/core'; import { FileService } from '../file.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-file-list', templateUrl: './file-list.component.html', styleUrls: ['./file-list.component.css'] }) consign form FileListComponent { public fileList$: Observable<string[]> = this.fileService.list(); constructor(private fileService: FileService) { } public download(fileName: string): void { this.fileService.download(fileName); } public remove(fileName: string): void { this.fileService.remove(fileName); } }
The information in the fileList$
appreciable will exist displayed on a list that also includes clickable commands for downloading and removing each file.
Supercede the contents of the src/app/file-list/file-list.component.html file with the post-obit HTML markup:
<h1>Your files</h1> <ul> <li *ngFor="let fileName of fileList$ | async" > {{fileName}} <bridge (click)="download(fileName)">download</bridge> <span (click)="remove(fileName)">remove</span> </li> </ul>
The *ngFor
loop iterates through the list of files from the fileList$
appreciable, which emits an array of strings. A <li>
chemical element containing <bridge>
elements bound to download()
and remove()
operations will be created for each entry.
CSS can exist used to indicate that the commands contained in the spans are clickable.
Insert the following CSS code into the src/app/file-list/file-listing.component.css file:
bridge:hover { cursor: pointer; }
The FileListComponent
class and the FileUploaderComponent
class accept to be included in the principal component of the application, AppComponent
, to be rendered in the browser.
Replace the contents of the src/app/app.component.html with the following HTML markup:
<app-file-list></app-file-list> <app-file-uploader></app-file-uploader>
Test the bones awarding
Execute the following Angular CLI command in angular-and-nodejs-information to build and run the application:
Open a browser tab and navigate to http://localhost:4200. You should run into an empty file list and a form ready for user input, like the i shown below:
Choose a suitable file and click the upload button. The name of the selected file should appear in the file list, equally in the example shown below:
Effort clicking download. You will see that nothing happens.
Try clicking remove. The file name should exist removed from the list.
At this point the application enables users to select files and "upload" them, but they are only "uploaded" as far as the list of files in memory on the client machine. Files can also be removed from the list in retention.
This isn't very useful, simply it'south plenty to show you how the user interface and the file list work.
If y'all want to catch up to this pace using the code from the GitHub repository, execute the following commands in the directory where y'all'd like to create the project directory:
git clone https://github.com/maciejtreder/angular-and-nodejs-information.git cd angular-and-nodejs-data git checkout step1 npm install
Save files on the server
The side by side step is to transfer files to the server and store them on disk. Yous'll do that by calculation more functionality to the FileService
class.
Outset you need to add the Node.js server to the project and create a folder dedicated to storing user uploads.
In the angular-and-nodejs -information folder, execute the following instructions at the control line:
ng add @ng-toolkit/universal mkdir user_upload
Installing the @ng-toolkit/universal
project added Angular Universal support to the project with just 1 command. Information technology as well includes a Node.js back end and server-side rendering (SSR). You can read more than almost SSR in Angular and its implications for search engine optimization (SEO) in this post.
Implement RESTful API endpoints in the server lawmaking
API endpoints volition provide file treatment on the server, so there are a few modifications to brand to the server.ts file. They include adding fs
module back up (for manipulating the file system) and specifying a catalog in which to store information.
Open up the server.ts file and find the following constant declaration:
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');
Add the post-obit constant declarations immediately after the line above:
const userFiles = './user_upload/'; const fs = require('fs');
Implement the /upload endpoint, which will be consumed past the front-end application.
In the server.ts file, find the following line of code:
app.prepare('views', './dist/browser');
Add the following TypeScript code to the server.ts file immediately post-obit the line in a higher place:
app.put('/files', (req, res) => { const file = req.body; const base64data = file.content.replace(/^data:.*,/, ''); fs.writeFile(userFiles + file.proper noun, base64data, 'base64', (err) => { if (err) { console.log(err); res.sendStatus(500); } else { res.ready('Location', userFiles + file.name); res.status(200); res.send(file); } }); });
Because we are going to upload Base64 encoded data in the request body, we need to adjust the maximum body size.
Near the pinnacle of the server.ts file, notice the following line of code:
app.employ(bodyParser.json());
Replace the line in a higher place with the following TypeScript code:
app.use(bodyParser.json({limit: '50mb'}));
Implement the /delete endpoint.
Add together the post-obit TypeScript lawmaking to the bottom of the server.ts file:
app.delete('/files/**', (req, res) => { const fileName = req.url.substring(7).replace(/%20/g, ' '); fs.unlink(userFiles + fileName, (err) => { if (err) { console.log(err); res.sendStatus(500); } else { res.status(204); res.ship({}); } }); });
Implement the Go /files endpoint.
Add the following line of TypeScript lawmaking to the lesser of the server.ts file:
app.use('/files', express.static(userFiles));
Using the express.static
method informs Node.js that every GET request sent to the /files/** endpoint should be treated as "static" hosting, served from the userFiles
directory, user_upload.
These RESTful API endpoints in the server tin can now be consumed in the front end-cease Angular application.
Supervene upon the contents of the src/app/file.service.ts file with the post-obit TypeScript code:
import { Injectable } from '@angular/core'; import { BehaviorSubject, Subject, Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; import { HttpClient } from '@athwart/common/http'; @Injectable({ providedIn: 'root' }) export grade FileService { private fileList: string[] = new Array<cord>(); private fileList$: Discipline<string[]> = new Subject<string[]>(); private displayLoader$: Subject<boolean> = new BehaviorSubject<boolean>(simulated); constructor(private http: HttpClient) { } public isLoading(): Observable<boolean> { return this.displayLoader$; } public upload(fileName: cord, fileContent: string): void { this.displayLoader$.side by side(true); this.http.put('/files', {name: fileName, content: fileContent}) .pipage(finalize(() => this.displayLoader$.next(fake))) .subscribe(res => { this.fileList.button(fileName); this.fileList$.next(this.fileList); }, mistake => { this.displayLoader$.next(false); }); } public download(fileName: string): void { this.http.become('/files/${fileName}', { responseType: 'blob'}).subscribe(res => { window.open(window.URL.createObjectURL(res)); }); } public remove(fileName): void { this.http.delete('/files/${fileName}').subscribe(() => { this.fileList.splice(this.fileList.findIndex(proper noun => name === fileName), i); this.fileList$.next(this.fileList); }); } public listing(): Observable<string[]> { return this.fileList$; } private addFileToList(fileName: string): void { this.fileList.push(fileName); this.fileList$.next(this.fileList); } }
The lawmaking above fully implements the file operations to upload, download, and remove files. Information technology also adds the isLoading()
method, which returns an observable-emitting boolean value indicating if the action of uploading data is underway or not. The observable can be used in the AppComponent form to inform the user near the status of that activity.
Replace the contents of the src/app/app.component.ts with the following TypeScript code:
import { Component } from '@athwart/core'; import { FileService } from './file.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'angular-and-nodejs-information'; public displayLoader: Observable<boolean> = this.fileService.isLoading(); constructor(private fileService: FileService) {} }
When the value from the Observable
indicates an upload is in progress the app will brandish the following loader GIF (which is included in the GitHub repository):
Add the post-obit HTML markup to the bottom of the src/app/app.component.html file:
<img src="https://raw.githubusercontent.com/maciejtreder/angular-and-nodejs-data/step2/src/assets/loader.gif" *ngIf="displayLoader | async" />
Test uploading and downloading files
Rebuild the application and check if the upload and download functions piece of work properly.
Execute the following npm command line instructions in the angular-and-nodejs-data directory:
npm run build:prod npm run server
Open a browser tab and navigate to http://localhost:8080. Cull a file and upload information technology.
The file name should be displayed in the file listing nether Your files and also be present in the user_upload directory.
Yous should besides exist able to download the file by clicking download. Note that your browser may open the file in a new tab or window instead of downloading information technology, based on the file type and your browser settings. The following illustration demonstrates the complete sequence:
Click remove and verify the file name is removed from the listing under Your files and the file itself is removed from the user_upload directory.
If you want to catch up to this step using the code from the GitHub repository, execute the following commands in the directory where you'd like to create the project directory:
git clone https://github.com/maciejtreder/athwart-and-nodejs-data.git cd angular-and-nodejs-data git checkout step2 npm install
Retrieve and brandish a file list
You are nearly washed. The application supports uploading a new file to the storage, retrieving it, and removing it. The problem occurs when a user navigates dorsum to the awarding.
You can simulate that beliefs. If y'all still have http://localhost:8080 opened in your browser, striking the refresh button. The list of files is gone! Merely they are nonetheless on the server in the user_upload directory.
The next footstep in this project is to implement a responsive list of files for the user_upload directory. The listing shown in the browser window will be updated dynamically to reflect the contents of the directory and it volition reverberate the list of files in the directory when the application starts.
It's possible to practise that past adding another REST endpoint to our server that returns a list of files. This would exist adept solution when the back-end server code is running on a dissimilar machine from the code that does the server-side rendering.
Only equally long as the back-end code is running on the aforementioned server as the code that serves the front cease, it doesn't make sense to execute the Angular Universal (server-side rendering) lawmaking and execute Rest calls to the same machine. Instead, you can use the fs
module to list all files in a given path.
The previous mail service, Build Faster JavaScript Web Apps with Angular Universal, a TransferState Service and an API Watchdog, demonstrates how to implement isPlatformServer()
and isPlatformBrowser()
methods to determine which platform is executing the code. This project uses those functions too.
The previous mail also shows how to share data between the server and the client with the TransferState
object by injecting it into the AuthService
class. These methods help brand fs
module functionality accessible to the client-side lawmaking, even though the module itself can't be loaded in the browser. This project also utilizes that technique.
The post-obit diagram shows the sequence of events:
At that place is one more thing to consider hither. You know that browsers volition not let JavaScript code to manipulate the file system for security reasons. The webpack JavaScript module bundling system used past the Athwart CLI won't allow you to employ the fs
module for lawmaking built for the browser.
Since this project has a unmarried codebase for both platforms, webpack interprets it as beingness congenital for the browser—which it is, in office. Only information technology needs fs
to read the directory contents and dispense files, so it needs a solution that will go around the prohibition on running fs
in the browser.
At this indicate you might recall y'all need to create a separate codebase just for the server-side code, giving you lot two projects to maintain. But at that place is a technique which tin can enable you lot to maintain the unmarried codebase and still manipulate files from the Angular executed on the server.
Angular has the ability to inject values and references outside the "Angular sandbox". You can laissez passer a reference to the Node.js part to the angular-side code and execute it from there.
Accept a look at the following diagram:
Implement server-side file manipulation
With the API endpoints in identify you can consummate the implementation of file manipulation operations from the client.
Open the server.ts file and locate the following line of code:
const fs = require('fs');
Insert the following TypeScript code nether the line above:
const listFiles = (callBack) => { return fs.readdir('./user_upload', callBack); };
Locate the post-obit lawmaking in the server.ts file:
app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] }));
Change the code shown to a higher place to include the additional line shown below:
app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP), {provide: 'LIST_FILES', useValue: listFiles} ] }));
Now comes the time to eat this server function in the Athwart application.
Open the src/app/file.service.ts file and replace the existing import
directives with the following TypeScript code:
import { HttpClient } from '@athwart/common/http'; import { Injectable, Inject, PLATFORM_ID, Optional } from '@athwart/cadre'; import { BehaviorSubject, Subject, Appreciable, ReplaySubject } from 'rxjs'; import { finalize } from 'rxjs/operators'; import { isPlatformServer } from '@athwart/mutual'; import { TransferState, makeStateKey, StateKey } from '@athwart/platform-browser';
To get in possible for the file list displayed on the folio to include all the files in the directory, the observable blazon for fileList$
needs to be changed to a ReplaySubject
, an observable that makes available to its subscribers a list of the values previously emitted to it. This enables the observer to go the list of files added to the observable earlier the observer subscribes to the appreciable. According to the RxJS documentation: "ReplaySubject
emits to any observer all of the items that were emitted by the source Appreciable(southward), regardless of when the observer subscribes."
Find the post-obit line of code in the src/app/file.service.ts file:
private fileList$: Subject area<string[]> = new Bailiwick<cord[]>();
Supersede the line above with the following TypeScript code:
private fileList$: Subject<string[]> = new ReplaySubject<string[]>(1);
Alter the FileService
constructor to provide the course with the PLATFORM_ID
(client or server) and the TransferState
object. If the code is running on the server the constructor logic reads the contents of the user_upload directory (by using injected reference to the listFiles
method) and adds the list of files to the TransferState
object. If the lawmaking is running on the client, the listing of files in transferState
is copied to the class' private member variable, fileList
.
Find the line of code below in the src/app/file.service.ts file:
constructor(private http: HttpClient) { }
Replace the line above with the following TypeScript code:
constructor( private http: HttpClient, @Optional() @Inject('LIST_FILES') individual listFiles: (callback) => void, @Inject(PLATFORM_ID) private platformId: any, private transferState: TransferState ) { const transferKey: StateKey<cord> = makeStateKey<cord>('fileList'); if (isPlatformServer(this.platformId)) { this.listFiles((err, files) => { this.fileList = files; this.transferState.set(transferKey, this.fileList); }); } else { this.fileList = this.transferState.become<string[]>(transferKey, []); } this.fileList$.side by side(this.fileList); }
Exam the consummate application
Rebuild the awarding past executing the following educational activity at the command line in the athwart-and-nodejs-data directory:
npm run build:prod npm run server
Open a browser window and navigate to http://localhost:8080. Any files in the user_upload directory should be listed under Your files, as shown below, and you should exist able to upload, download, and remove files from the server.
If yous desire to catch upward to this step using the code from the GitHub repository, execute the following commands in the directory where you'd like to create the project directory:
git clone https://github.com/maciejtreder/athwart-and-nodejs-data.git cd angular-and-nodejs-information git checkout step3 npm install
What about security?
Does Angular running on the client have admission to data exterior of it, similar the server file organisation? Yes information technology does. And y'all take same codebase for the server and browser? Yes you do.
You lot might ask: "What virtually path traversal? Tin can everyone on the internet see the data I store in the user_upload directory?" This question is more than advisable here!
What we are doing in our app is passing reference to the method, not method itself. That's why providing data from Node.js to the Angular client app is a great manner of sharing sensitive data.
Examine the build output and accept a look at the FileService
constructor in the dist/primary.hashcode.js file:
function e(e,t,due north,r){ var o=this; this.http=e, this.listFiles=t, this.platformId=northward, this.transferState=r, this.fileList=new Array, this.fileList$=new oa(1),this.displayLoader$=new sa(!1); var i=Bu("fileList"); Oa(this.platformId)? this.listFiles(function(due east,t){ o.fileList=t,o.transferState.set(i,o.fileList) }) : this.fileList=this.transferState.get(i,[]),this.fileList$.next(this.fileList) }
As yous can see, the JavaScript is expecting Node.js to laissez passer a reference to the part, passed as the variable t
. No information near directory structure on our server tin be retrieved from the JavaScript in the output bundle.
Summary of passing data from Node.js to Angular
In this projection you lot learned how to transfer files betwixt a client browser and a Node.js server in a single project codebase. The client's user interface tin select files to upload, upload them to the server where they are stored, list the files stored on the server, remove files stored on the server, and download files from the server. Y'all saw how to do all this in a unmarried Angular codebase using Angular Universal and Node.js. You as well saw that this is a secure method of transferring data between the client and server, including files stored on the server.
Additional resources
Angular Universal documentation, including tutorials and the CLI reference
Dependency Injection in Action in Angular
TransferState form documentation, part of the @athwart/platform-browser
ReplaySubject objects explained with other Subject object variants
RxJS ReplaySubject documentation, a "work in progress"
Maciej Treder is a Senior Software Evolution Engineer at Akamai Technologies . He is as well an international conference speaker and the author of @ng-toolkit , an open source toolkit for edifice Angular progressive web apps (PWAs), serverless apps, and Angular Universal apps. Bank check out the repo to larn more almost the toolkit, contribute, and support the project. You can learn more about the author on his website . Yous tin can too contact him at: contact@maciejtreder.com or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.
Source: https://www.twilio.com/blog/transfer-files-data-javascript-applications-angular-node-js
0 Response to "Download Uploaded Files Are Broken in Angular"
Post a Comment