Introduction

This article shows how to build a production-ready File Manager: a secure API for storing and serving files, and an Angular UI for uploading, browsing, previewing, and managing files. The guide focuses on practical code you can copy and run, with attention to security, scalability, and real-world concerns such as chunked uploads, streaming downloads, thumbnails and access control.

The writing style is simple Indian English and targeted at senior developers.

High-level architecture

[Angular SPA] <--- HTTPS/JWT ---> [ASP.NET Core API] <---> [Blob Storage (S3 / Azure Blob)]
                                               |
                                               +--> [SQL / Metadata store]
                                               |
                                               +--> [Virus Scan Service] (optional)
                                               |
                                               +--> [CDN for public assets]

Responsibilities

Storage choices and trade-offs

Use blob storage for file content and keep small metadata in SQL Server or PostgreSQL.

Database model (simple)

Files(
  Id GUID PK,
  FileName nvarchar(512),
  ContentType nvarchar(128),
  Size bigint,
  OwnerId GUID,
  StoragePath nvarchar(1024),
  Hash varchar(64),
  IsPublic bit,
  CreatedAt datetimeoffset,
  UpdatedAt datetimeoffset
)

FilePermissions(
  FileId FK,
  UserId FK,
  CanRead bit,
  CanWrite bit,
  CanDelete bit
)

Index on OwnerId, CreatedAt, and FileName (include full-text or trigram index for search).

Backend: Project setup

Create ASP.NET Core Web API project and add packages:

dotnet new webapi -n FileManagerApi
cd FileManagerApi
dotnet add package Azure.Storage.Blobs   // or AWSSDK.S3
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Swashbuckle.AspNetCore  // Swagger

Register DbContext, authentication (JWT), CORS and blob client in Program.cs.

Backend: File entity and DTOs

public class FileEntity
{
    public Guid Id { get; set; }
    public string FileName { get; set; } = string.Empty;
    public string ContentType { get; set; } = string.Empty;
    public long Size { get; set; }
    public Guid OwnerId { get; set; }
    public string StoragePath { get; set; } = string.Empty; // blob path
    public string? Hash { get; set; }
    public bool IsPublic { get; set; }
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}

public class FileUploadResultDto { public Guid Id { get; set; } public string FileName { get; set; } public string Url { get; set; } }

Backend: Storage service (abstraction)

Create an interface IFileStorage and two implementations: AzureBlobStorage (production) and LocalFileStorage (dev). Core methods:

Task<string> UploadAsync(Stream content, string path, string contentType, CancellationToken ct);
Task<Stream> DownloadAsync(string path, CancellationToken ct);
Task DeleteAsync(string path, CancellationToken ct);
Task GenerateThumbnailAsync(string sourcePath, string thumbPath);
Task<string> CreatePresignedUrlAsync(string path, TimeSpan expiry);

Keep the blob client usage inside the storage implementation. This keeps controller code clean and testable.

Backend: Controller endpoints (examples)

FilesController endpoints:

Sample upload action (simple)

[HttpPost]
[Authorize]
public async Task<IActionResult> Upload(IFormFile file)
{
    if (file == null || file.Length == 0) return BadRequest();

    var ownerId = User.GetUserId();
    // server-side validations
    if (file.Length > _options.MaxFileSize) return StatusCode(413);

    var id = Guid.NewGuid();
    var storagePath = $"files/{id}/{file.FileName}";
    await _storage.UploadAsync(file.OpenReadStream(), storagePath, file.ContentType, HttpContext.RequestAborted);

    var entity = new FileEntity { Id = id, FileName = file.FileName, ContentType = file.ContentType, Size = file.Length, OwnerId = ownerId, StoragePath = storagePath };
    _db.Files.Add(entity);
    await _db.SaveChangesAsync();

    var url = await _storage.CreatePresignedUrlAsync(storagePath, TimeSpan.FromMinutes(60));

    return Ok(new FileUploadResultDto { Id = id, FileName = file.FileName, Url = url });
}

Chunked upload flow

Use storage-native multipart APIs to avoid re-assembling in app memory.

Backend: Security and validations

Backend: Streaming downloads and partial content

For large files support Range header to allow resume and streaming. Example:

[HttpGet("{id}/download")]
public async Task<IActionResult> Download(Guid id)
{
    var file = await _db.Files.FindAsync(id);
    if (file == null) return NotFound();

    var stream = await _storage.DownloadAsync(file.StoragePath, HttpContext.RequestAborted);
    return File(stream, file.ContentType, enableRangeProcessing: true, file.FileName);
}

enableRangeProcessing: true lets ASP.NET Core handle Range and partial content.

Backend: Thumbnail generation

Frontend: Angular setup and libraries

Install example packages

npm install ngx-file-drop @azure/storage-blob

Frontend: FileService (Angular)

file.service.ts responsibilities:

Example simplified upload (small files)

upload(file: File) {
  const fd = new FormData();
  fd.append('file', file);
  return this.http.post<FileUploadResult>('/api/files', fd);
}

For direct-to-blob using presigned URL:

  1. Call POST /api/files/presign with metadata. Server returns presigned URL.

  2. Use fetch or HttpClient.put to upload file directly to blob storage.

  3. Notify API POST /api/files/confirm to persist metadata.

This avoids proxying binary through API and improves throughput.

Frontend: Chunked upload example (basic)

async uploadInChunks(file: File) {
  const chunkSize = 5 * 1024 * 1024; // 5MB
  const uploadInit = await this.http.post('/api/files/chunk/init', { fileName: file.name, size: file.size }).toPromise();
  const uploadId = uploadInit.uploadId;
  const totalChunks = Math.ceil(file.size / chunkSize);

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    await this.http.put(`/api/files/chunk/${uploadId}/${i}`, chunk, { headers: { 'Content-Type': 'application/octet-stream' } }).toPromise();
  }

  await this.http.post(`/api/files/chunk/complete/${uploadId}`, {}).toPromise();
}

Handle retry for each chunk and resume by asking server which chunks already uploaded.

Frontend: UI components

UX notes

Security: Sharing and access control

Scaling and CDN

Monitoring and observability

Testing and deployment

Example: Minimal FilesController (abridged)

[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
    private readonly IFileStorage _storage;
    private readonly AppDbContext _db;

    public FilesController(IFileStorage storage, AppDbContext db) { _storage = storage; _db = db; }

    [HttpPost]
    [Authorize]
    public async Task<IActionResult> Upload(IFormFile file)
    {
        if (file == null || file.Length == 0) return BadRequest();
        var ownerId = User.GetUserId();
        // validations omitted for brevity
        var id = Guid.NewGuid();
        var storagePath = $"files/{id}/{file.FileName}";
        await _storage.UploadAsync(file.OpenReadStream(), storagePath, file.ContentType, CancellationToken.None);
        var entity = new FileEntity { Id = id, FileName = file.FileName, ContentType = file.ContentType, Size = file.Length, OwnerId = ownerId, StoragePath = storagePath };
        _db.Files.Add(entity);
        await _db.SaveChangesAsync();
        var url = await _storage.CreatePresignedUrlAsync(storagePath, TimeSpan.FromMinutes(60));
        return Ok(new FileUploadResultDto { Id = id, FileName = file.FileName, Url = url });
    }

    [HttpGet("{id}/download")]
    public async Task<IActionResult> Download(Guid id)
    {
        var file = await _db.Files.FindAsync(id);
        if (file == null) return NotFound();
        var stream = await _storage.DownloadAsync(file.StoragePath, CancellationToken.None);
        return File(stream, file.ContentType, file.FileName, enableRangeProcessing: true);
    }
}

Final best practices (short)

Next steps

I can now:

Which one would you like next?