For an example application that integrates this process, see my FullstackOverview repo on GitHub. The code in this example is taken directly from this app.
This component allows you have a custom styled file upload element.
file-upload.component.ts
import {
Component,
EventEmitter,
Input,
Output,
ViewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'file-upload',
templateUrl: 'file-upload.component.html',
styleUrls: ['file-upload.component.css']
})
export class FileUploadComponent {
@ViewChild('fileInput') fileInput: ElementRef;
@Input() accept = '*/*';
@Input() color = 'primary';
@Input() label = 'Browse...';
@Input() multiple = true;
@Output() selected = new EventEmitter<[File[], FormData]>();
fileChange = (event: any) => {
const files: FileList = event.target.files;
const fileList = new Array<File>();
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append(files.item(i).name, files.item(i));
fileList.push(files.item(i));
}
this.selected.emit([fileList, formData]);
this.fileInput.nativeElement.value = null;
}
}
file-upload.component.html
<input type="file"
(change)="fileChange($event)"
#fileInput
[accept]="accept"
[multiple]="multiple">
<button mat-button
[color]="color"
(click)="fileInput.click()">{{label}}</button>
file-upload.component.css
input[type=file] {
display: none;
}
This shows the pieces of a component (with everything else left out) that make use of the above component in order to enable file uploads.
uploads.component.html
<mat-toolbar>
<span>Uploads</span>
<section class="toolbar-buttons"
[style.margin-left.px]="12">
<file-upload (selected)="fileChange($event)"
accept="image/*"></file-upload>
<button mat-button
color="primary"
(click)="uploadFiles()"
*ngIf="formData"
[disabled]="uploading">Upload</button>
<button mat-button
(click)="clearFiles()"
*ngIf="formData"
[disabled]="uploading">Cancel</button>
</section>
</mat-toolbar>
uploads.component.ts
import { Component } from '@angular/core';
import {
MatDialog
} from '@angular/material';
import {
IdentityService,
UploadService
} from '../../services';
import {
Upload,
User
} from '../../models';
@Component({
selector: 'uploads',
templateUrl: 'uploads.component.html',
providers: [UploadService]
})
export class UploadsComponent {
user: User;
files: File[];
formData: FormData;
uploading = false;
imgSize = 240;
constructor(
public identity: IdentityService,
public upload: UploadService
) { }
ngOnInit() {
this.identity.identity$.subscribe(auth => {
if (auth.user) {
this.user = auth.user;
this.upload.getUserUploads(this.user.id);
}
});
}
fileChange(fileDetails: [File[], FormData]) {
this.files = fileDetails[0];
this.formData = fileDetails[1];
}
clearFiles() {
this.files = null;
this.formData = null;
}
async uploadFiles() {
this.uploading = true;
const res = await this.upload.uploadFiles(this.formData, this.user.id);
this.uploading = false;
this.clearFiles();
res && this.upload.getUserUploads(this.user.id);
}
}
I have a CoreService
Angular service that has a getUploadOptions()
function that returns an HttpHeaders
object that adjusts the headers for file uploads:
getUploadOptions = (): HttpHeaders => {
const headers = new HttpHeaders();
headers.set('Accept', 'application/json');
headers.delete('Content-Type');
return headers;
}
upload.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { CoreService } from './core.service';
import { ObjectMapService } from './object-map.service';
import { SnackerService } from './snacker.service';
import { Upload } from '../models';
@Injectable()
export class UploadService {
private uploads = new BehaviorSubject<Upload[]>(null);
uploads$ = this.uploads.asObservable();
constructor(
private http: HttpClient,
private core: CoreService,
private snacker: SnackerService
) { }
getUserUploads = (userId: number) =>
this.http.get<Upload[]>(`/api/upload/getUserUploads/${userId}`)
.subscribe(
data => this.uploads.next(data),
err => this.snacker.sendErrorMessage(err.error)
);
uploadFiles = (formData: FormData, userId: number): Promise<boolean> =>
new Promise((resolve) => {
this.http.post(
`/api/upload/uploadFiles/${userId}`,
formData,
{ headers: this.core.getUploadOptions() }
)
.subscribe(
() => {
this.snacker.sendSuccessMessage('Uploads successfully processed');
resolve(true);
},
err => {
this.snacker.sendErrorMessage(err.error);
resolve(false);
}
)
});
}
In this app, I've made the Directory the file will be uploaded to, as well as the URL base it will be referenced from, configurable via a class called UploadConfig
:
UploadConfig.cs
public class UploadConfig
{
public string DirectoryBasePath { get; set; }
public string UrlBasePath { get; set; }
}
It gets registered as a Singleton in Startup.cs ConfigureServices()
method:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Additional Configuration
if (Environment.IsDevelopment())
{
services.AddSingleton(new UploadConfig
{
DirectoryBasePath = $@"{Environment.ContentRootPath}\wwwroot\",
UrlBasePath = "/"
});
}
else
{
services.AddSingleton(new UploadConfig
{
DirectoryBasePath = Configuration.GetValue<string>("AppDirectoryBasePath"),
UrlBasePath = Configuration.GetValue<string>("AppUrlBasePath")
});
}
// Additional Configuration
}
An API Controller receives the User ID of the current user in the URL path of an HTTP Post, and the uploads are retrieved from the FormData
posted to the controller:
UploadController.cs
[Route("api/[controller]")]
public class UploadController : Controller
{
public AppDbContext db;
public UploadConfig config;
public UploadController(AppDbContext db, UploadConfig config)
{
this.db = db;
this.config = config;
}
[HttpPost("[action]/{id}")]
[DisableRequestSizeLimit]
public async Task<List<Upload>> UploadFiles([FromRoute]int id)
{
var files = Request.Form.Files;
if (files.Count < 1)
{
throw new Exception("No files provided for upload");
}
return await db.UploadFiles(files, config.DirectoryBasePath, config.UrlBasePath, id);
}
}
The actual methods that comprise uploading files:
UploadExtensions.cs
public static async Task<List<Upload>> UploadFiles(this AppDbContext db, IFormFileCollection files, string path, string url, int userId)
{
var uploads = new List<Upload>();
foreach (var file in files)
{
uploads.Add(await db.AddUpload(file, path, url, userId));
}
return uploads;
}
static async Task<Upload> AddUpload(this AppDbContext db, IFormFile file, string path, string url, int userId)
{
var upload = await file.WriteFile(path, url);
upload.UserId = userId;
upload.UploadDate = DateTime.Now;
await db.Uploads.AddAsync(upload);
await db.SaveChangesAsync();
return upload;
}
static async Task<Upload> WriteFile(this IFormFile file, string path, string url)
{
if (!(Directory.Exists(path)))
{
Directory.CreateDirectory(path);
}
var upload = await file.CreateUpload(path, url);
using (var stream = new FileStream(upload.Path, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return upload;
}
static Task<Upload> CreateUpload(this IFormFile file, string path, string url) => Task.Run(() =>
{
var name = file.CreateSafeName(path);
var upload = new Upload
{
File = name,
Name = file.Name,
Path = $"{path}{name}",
Url = $"{url}{name}"
};
return upload;
});
static string CreateSafeName(this IFormFile file, string path)
{
var increment = 0;
var fileName = file.FileName.UrlEncode();
var newName = fileName;
while (File.Exists(path + newName))
{
var extension = fileName.Split('.').Last();
newName = $"{fileName.Replace($".{extension}", "")}_{++increment}.{extension}";
}
return newName;
}
private static readonly string urlPattern = "[^a-zA-Z0-9-.]";
static string UrlEncode(this string url)
{
var friendlyUrl = Regex.Replace(url, @"\s", "-").ToLower();
friendlyUrl = Regex.Replace(friendlyUrl, urlPattern, string.Empty);
return friendlyUrl;
}
Your use of var is vexing because you don't show the actual types.