Skip to content

FILES MODULE

María Fátima García Luque edited this page Jun 25, 2026 · 1 revision

Files Module — @fireflyframework/core/files

Complete file management on the front-end: validation, native picker selection, upload with progress, download (URL and Blob), preview, and tabular export (CSV, PDF, Excel). Package: @fireflyframework/core · EXTENDED Module #1 · Platform + Embedded


Table of Contents


1. Overview

The Files module groups six services that cover the complete lifecycle of a file in a front-end application: validate, select, upload, download, preview, and export.

What it does

  • FileValidationService — validates files against configurable rules (size, MIME type, extension) before processing them. Supports per-call rules and module-level defaults
  • FilePickerService — opens the browser's native file picker programmatically, returning a Promise<File[]>. Includes a pickImage() shortcut for image selection
  • FileUploadService — uploads files via HTTP POST with real-time progress tracking (Observable<UploadProgress>). Supports single and concurrent multiple uploads
  • FileDownloadService — downloads files from a URL or from an in-memory Blob using the hidden anchor technique. Automatically revokes blob URLs to prevent memory leaks
  • FilePreviewService — manages the lifecycle of blob URLs for preview. Opens previews in a new tab and tracks active URLs for cleanup
  • FileExportService — exports tabular data (arrays of objects) to CSV, PDF, or Excel. Generates the Blob and delegates download to FileDownloadService

What it does NOT do

  • It does not implement dropzone UI, progress bars, or preview modals — that is the responsibility of the product layer
  • It does not manage persistent server-side storage — it only operates in the browser
  • It does not implement chunked or resumable uploads — uploads use standard HTTP POST
  • It does not generate complex PDFs with advanced styling — PDF export is basic tabular
  • It does not handle image compression or thumbnails
  • It does not implement drag-and-drop — products use the native picker or their own D&D solution

Why it is part of the framework

Every product needs to work with files: upload documents, download reports, export data, validate attachments. Without a unified module, each product reimplements these capabilities with inconsistent patterns, poor memory management (blob URL leaks), and no standardization in validation.

The Files module provides a headless service layer that:

  • Standardizes validation (size, type, extension)
  • Centralizes configuration via provideFiles() with sensible defaults
  • Guarantees memory cleanup (blob URL tracking)
  • Exposes reactive APIs (Observables for upload, Promises for picker/export)

Key architectural decision

EXTENDED module (not CORE): Unlike core modules that register automatically, Files requires an explicit call to provideFiles(). This is because not every application needs file management, and the services depend on HttpClient (upload) and DOCUMENT (picker, download, preview), which only make sense in browser contexts.

The services do NOT use providedIn: 'root' — they are registered via makeEnvironmentProviders when calling provideFiles(). This avoids unnecessary instantiation in products that do not use files.

Relationship with Flutter

In the original Flutter portal (soon-distributor-portal), file management was scattered:

  • file_picker package for selection
  • Manual validation logic on every screen
  • dio for uploads with progress callback
  • url_launcher for downloads
  • csv package for export

Firefly consolidates everything into a cohesive module with centralized configuration, standardized validation, and automatic memory management, eliminating the fragmentation that existed in Flutter.


2. Developer Usage Guide

2.1 Installation and initial configuration

The Files module is EXTENDED — it requires explicit registration:

// app.config.ts
import { provideFiles } from '@fireflyframework/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideFiles({
      maxFileSizeBytes: 5 * 1024 * 1024,       // 5 MB
      allowedMimeTypes: ['image/png', 'image/jpeg', 'application/pdf'],
      maxConcurrentUploads: 3,
      enablePreview: true,
    }),
  ],
};

All fields are optional. Defaults:

Field Default
maxFileSizeBytes 10485760 (10 MB)
allowedMimeTypes [] (no restriction)
maxConcurrentUploads 5
uploadEndpoint undefined
enablePreview true

With no arguments (provideFiles()), the services are registered with defaults.

2.2 File validation

import { FileValidationService } from '@fireflyframework/core';

@Component({ /* ... */ })
export class UploadComponent {
  private readonly validator = inject(FileValidationService);

  onFileSelected(file: File): void {
    // Uses module defaults (from provideFiles config)
    const result = this.validator.validate(file);

    if (!result.valid) {
      console.error('Errors:', result.errors);
      return;
    }

    // Or use call-specific rules (these take precedence)
    const strictResult = this.validator.validate(file, {
      maxSizeBytes: 2 * 1024 * 1024,
      allowedMimeTypes: ['application/pdf'],
      allowedExtensions: ['.pdf'],
    });
  }
}

Rule precedence: rules passed by parameter (FileValidationConfig) take precedence over module defaults (FilesConfig). If no rules are passed, the defaults are applied.

2.3 File selection (Picker)

import { FilePickerService } from '@fireflyframework/core';

@Component({ /* ... */ })
export class FileSelectComponent {
  private readonly picker = inject(FilePickerService);

  async selectDocument(): Promise<void> {
    const files = await this.picker.pickFile('application/pdf,.docx', false);
    if (files.length > 0) {
      console.log('Selected file:', files[0].name);
    }
  }

  async selectMultipleImages(): Promise<void> {
    const files = await this.picker.pickFile('image/*', true);
    console.log(`${files.length} images selected`);
  }

  async selectSingleImage(): Promise<void> {
    const image = await this.picker.pickImage();
    if (image) {
      console.log('Image:', image.name, image.size);
    }
  }
}

pickFile() returns an empty array if the user cancels. pickImage() returns null on cancel.

2.4 File upload

Single upload with progress

import { FileUploadService, UploadProgress } from '@fireflyframework/core';

@Component({ /* ... */ })
export class UploadComponent {
  private readonly uploader = inject(FileUploadService);
  progress = signal<UploadProgress | null>(null);

  upload(file: File): void {
    this.uploader.upload(file, '/api/files').subscribe({
      next: (p) => {
        this.progress.set(p);
        console.log(`${p.percent}% — ${p.loadedBytes}/${p.totalBytes}`);
      },
      error: (err) => console.error('Upload failed:', err),
    });
  }
}

Multiple upload

upload(files: File[]): void {
  this.uploader.uploadMultiple(files, '/api/files').subscribe({
    next: (progressArray) => {
      // progressArray: UploadProgress[] — one per file
      progressArray.forEach((p, i) => {
        console.log(`File ${i}: ${p.percent}% (${p.status})`);
      });
    },
    complete: () => console.log('All uploads complete'),
  });
}

Custom headers

this.uploader.upload(file, '/api/files', {
  'Authorization': 'Bearer my-token',
  'X-Custom-Header': 'value',
}).subscribe(/* ... */);

2.5 File download

From a URL

import { FileDownloadService } from '@fireflyframework/core';

@Component({ /* ... */ })
export class DownloadComponent {
  private readonly downloader = inject(FileDownloadService);

  downloadReport(): void {
    this.downloader.download('/api/reports/123', 'report-2024.pdf');
  }
}

From an in-memory Blob

downloadGenerated(): void {
  const content = JSON.stringify({ data: 'example' }, null, 2);
  const blob = new Blob([content], { type: 'application/json' });
  this.downloader.downloadBlob(blob, 'data.json');
}

downloadBlob() creates a temporary blob URL, triggers the download, and revokes the URL automatically.

2.6 File preview

import { FilePreviewService } from '@fireflyframework/core';

@Component({ /* ... */ })
export class PreviewComponent implements OnDestroy {
  private readonly preview = inject(FilePreviewService);

  previewPdf(blob: Blob): void {
    this.preview.previewInNewTab(blob, 'application/pdf');
  }

  // For use in templates (e.g. <img [src]=imageUrl>)
  imageUrl: string | null = null;

  showImage(blob: Blob): void {
    this.imageUrl = this.preview.createBlobUrl(blob);
  }

  ngOnDestroy(): void {
    // Cleans up all blob URLs when the component is destroyed
    this.preview.revokeAll();
  }
}

previewInNewTab() returns null if enablePreview is disabled in the configuration.

activeUrlCount exposes the number of active blob URLs (useful for debugging memory leaks).

2.7 Tabular export

import { FileExportService, ExportConfig } from '@fireflyframework/core';

interface User {
  name: string;
  email: string;
  role: string;
}

@Component({ /* ... */ })
export class ExportComponent {
  private readonly exporter = inject(FileExportService);

  async exportUsers(users: User[]): Promise<void> {
    await this.exporter.export(users, {
      format: 'csv',
      filename: 'users-report',
      columns: [
        { key: 'name', header: 'Full Name' },
        { key: 'email', header: 'Email Address' },
        { key: 'role', header: 'Role' },
      ],
      csvSeparator: ';',
    });
  }

  // Or generate the Blob without downloading (for preview)
  async generateExcel(users: User[]): Promise<Blob> {
    return this.exporter.generateBlob(users, {
      format: 'xlsx',
      filename: 'users',
      columns: [
        { key: 'name', header: 'Name', width: 30 },
        { key: 'email', header: 'Email', width: 40 },
      ],
      title: 'User Report',
      sheetName: 'Users',
    });
  }
}

Supported formats: csv, pdf, xlsx.


3. API Reference

3.1 Types

FilesConfig

Module-level configuration, passed to provideFiles().

interface FilesConfig {
  readonly maxFileSizeBytes: number;
  readonly allowedMimeTypes: string[];
  readonly maxConcurrentUploads: number;
  readonly uploadEndpoint?: string;
  readonly enablePreview: boolean;
}
Field Type Default Description
maxFileSizeBytes number 10485760 Maximum size in bytes
allowedMimeTypes string[] [] Allowed MIME types. Empty array = no restriction
maxConcurrentUploads number 5 Maximum simultaneous uploads
uploadEndpoint string? undefined Default upload endpoint
enablePreview boolean true Enables preview

FileValidationConfig

Per-call validation rules.

interface FileValidationConfig {
  readonly maxSizeBytes?: number;
  readonly allowedMimeTypes?: string[];
  readonly allowedExtensions?: string[];
}
Field Type Description
maxSizeBytes number? Maximum size in bytes
allowedMimeTypes string[]? MIME types (supports wildcards: image/*)
allowedExtensions string[]? Extensions with the dot (.pdf, .png)

ValidationResult

interface ValidationResult {
  readonly valid: boolean;
  readonly errors: string[];
}

UploadProgress

Emitted by FileUploadService.upload() as Observable<UploadProgress>.

interface UploadProgress {
  readonly percent: number;         // 0-100
  readonly totalBytes?: number;     // undefined if the server does not report
  readonly loadedBytes: number;
  readonly status: 'pending' | 'uploading' | 'complete' | 'error';
  readonly response?: unknown;      // only when status === 'complete'
  readonly error?: string;          // only when status === 'error'
}

ExportFormat

type ExportFormat = 'csv' | 'pdf' | 'xlsx';

ExportColumn<T>

interface ExportColumn<T = unknown> {
  readonly key: keyof T & string;
  readonly header: string;
  readonly width?: number;     // chars (CSV), pts (PDF/Excel)
}

ExportConfig<T>

interface ExportConfig<T = unknown> {
  readonly format: ExportFormat;
  readonly filename: string;           // without extension
  readonly columns: ExportColumn<T>[];
  readonly title?: string;             // header in PDF, title in Excel
  readonly sheetName?: string;         // default: 'Sheet1'
  readonly csvSeparator?: string;      // default: ','
}

3.2 FileValidationService

Validates files against configurable rules. Checks MIME type, size, and extension.

class FileValidationService {
  validate(file: File, rules?: FileValidationConfig): ValidationResult;
}
Method Parameters Returns Description
validate file: File, rules?: FileValidationConfig ValidationResult Validates the file. Without rules, uses module defaults

Fallback behavior: if no rules are provided, the service uses maxFileSizeBytes and allowedMimeTypes from FilesConfig. Per-call rules always take precedence.

Wildcard support in MIME: image/* matches any image/png, image/jpeg, etc.

3.3 FilePickerService

Opens the browser's native file picker. Creates a hidden <input type=file>, clicks it, and resolves with the selected files. The input is removed from the DOM afterwards.

class FilePickerService {
  pickFile(accept?: string, multiple?: boolean): Promise<File[]>;
  pickImage(): Promise<File | null>;
}
Method Parameters Returns Description
pickFile accept?: string, multiple?: boolean Promise<File[]> Opens the picker. Returns [] on cancel
pickImage Promise<File | null> Shorthand for pickFile('image/*', false)

3.4 FileUploadService

Uploads files via HTTP POST with progress. Uses HttpClient with reportProgress: true.

class FileUploadService {
  upload(file: File, endpoint: string, headers?: Record<string, string>): Observable<UploadProgress>;
  uploadMultiple(files: File[], endpoint: string): Observable<UploadProgress[]>;
}
Method Parameters Returns Description
upload file, endpoint, headers? Observable<UploadProgress> Uploads a file with progress
uploadMultiple files, endpoint Observable<UploadProgress[]> Uploads multiple files, emits an array of progress

The upload uses FormData internally. The endpoint is required (there is no implicit fallback to uploadEndpoint from the config in the current public API).

3.5 FileDownloadService

Downloads files using the hidden anchor technique (<a download>).

class FileDownloadService {
  download(url: string, filename: string): void;
  downloadBlob(blob: Blob, filename: string): void;
}
Method Parameters Returns Description
download url, filename void Downloads from a URL
downloadBlob blob, filename void Downloads an in-memory Blob. Revokes the blob URL automatically

3.6 FilePreviewService

Manages blob URLs for preview. Tracks active URLs for cleanup.

class FilePreviewService {
  createBlobUrl(blob: Blob): string;
  revokeBlobUrl(url: string): void;
  revokeAll(): void;
  previewInNewTab(blob: Blob, mimeType?: string): string | null;
  get activeUrlCount(): number;
}
Method Parameters Returns Description
createBlobUrl blob string Creates a blob URL and tracks it
revokeBlobUrl url void Revokes a blob URL
revokeAll void Revokes all tracked URLs
previewInNewTab blob, mimeType? string | null Opens preview in a tab. null if preview is disabled
activeUrlCount number Number of active URLs (getter)

previewInNewTab() returns null (no-op) when enablePreview === false in FilesConfig.

3.7 FileExportService

Exports tabular data to a file. Selects the exporter according to the format and delegates the download to FileDownloadService.

class FileExportService {
  export<T>(data: T[], config: ExportConfig<T>): Promise<void>;
  generateBlob<T>(data: T[], config: ExportConfig<T>): Promise<Blob>;
}
Method Parameters Returns Description
export data, config Promise<void> Generates the file and triggers the download
generateBlob data, config Promise<Blob> Only generates the Blob (no download)

generateBlob() is useful for previewing the export or for additional processing before downloading.

3.8 provideFiles()

function provideFiles(config?: Partial<FilesConfig>): EnvironmentProviders;

Registers all services of the Files module and optionally configures FilesConfig.

  • Without arguments: uses sensible defaults
  • With Partial<FilesConfig>: merges with the defaults

EXTENDED pattern — does not use providedIn: 'root'. The services are only available if provideFiles() is called.

3.9 FILES_CONFIG

const FILES_CONFIG: InjectionToken<FilesConfig>;

Injection token for the module's resolved configuration. Injectable in any component or service after registering provideFiles().

private readonly config = inject(FILES_CONFIG);
console.log(this.config.maxFileSizeBytes); // resolved value

4. Internal Technical Architecture

4.1 Design principles

  1. Headless — the services have no UI. They provide pure operations that the product layer consumes to build its own UI
  2. Centralized configurationFilesConfig defines module-level defaults. The services read the token so the product does not have to pass config on every call
  3. Memory safetyFilePreviewService tracks every blob URL and exposes revokeAll() for cleanup. FileDownloadService.downloadBlob() revokes automatically
  4. Observable for I/O, Promise for actions — upload emits progress via an Observable (stream). Picker and export use Promise (one-shot action)
  5. No global state — the services do not store files or a history of operations. State lives in the component that consumes them

4.2 File structure

libs/framework-core/src/lib/files/
  index.ts                           # barrel export
  files.provider.ts                  # provideFiles(), FILES_CONFIG
  files.config.ts                    # FilesConfig interface, defaults
  services/
    file-validation.service.ts       # FileValidationService
    file-picker.service.ts           # FilePickerService
    file-upload.service.ts           # FileUploadService
    file-download.service.ts         # FileDownloadService
    file-preview.service.ts          # FilePreviewService
    file-export.service.ts           # FileExportService
  types/
    upload-progress.ts               # UploadProgress
    validation-result.ts             # ValidationResult, FileValidationConfig
    export-config.ts                 # ExportConfig, ExportColumn, ExportFormat

4.3 Data flow

[Product component]
    |
    |--- pickFile() ---> FilePickerService ---> <input type=file> ---> File[]
    |
    |--- validate(file) ---> FileValidationService ---> ValidationResult
    |                              |
    |                              +--- reads FilesConfig (fallback)
    |
    |--- upload(file, endpoint) ---> FileUploadService ---> HttpClient POST
    |                                     |                    (reportProgress)
    |                                     +--- Observable<UploadProgress>
    |
    |--- download(url, name) ---> FileDownloadService ---> <a download> click
    |--- downloadBlob(blob) ---> FileDownloadService ---> blob URL + <a> + revoke
    |
    |--- previewInNewTab(blob) ---> FilePreviewService ---> window.open(blobUrl)
    |--- createBlobUrl(blob) ---> FilePreviewService ---> URL.createObjectURL
    |
    |--- export(data, config) ---> FileExportService ---> generateBlob()
                                        |                      |
                                        +--- FileDownloadService.downloadBlob()

4.4 Internal dependencies

Service Injects Reason
FileValidationService FILES_CONFIG Reads validation defaults
FilePickerService DOCUMENT Creates <input> in the DOM
FileUploadService HttpClient, FILES_CONFIG POST with progress, reads config
FileDownloadService DOCUMENT Creates <a> in the DOM
FilePreviewService DOCUMENT, FILES_CONFIG URL.createObjectURL, reads enablePreview
FileExportService FileDownloadService Delegates download of the generated blob

External dependencies of the module:

  • @angular/coreinject, InjectionToken, makeEnvironmentProviders
  • @angular/commonDOCUMENT
  • @angular/common/httpHttpClient (only FileUploadService)
  • rxjsObservable (only FileUploadService)

4.5 Architectural decisions

Decision Discarded alternative Reason
EXTENDED module (not CORE) providedIn: 'root' on every service Not every app needs files. Avoids unnecessary instantiation
makeEnvironmentProviders NgModule with forRoot() Consistent with modern standalone Angular. No legacy modules
Observable for upload Promise with callback Upload has a natural progress stream. Observable enables RxJS operators (retry, cancellation)
Promise for picker Observable Picker is one-shot: the user selects and that's it. Promise is more natural
<a download> for downloading fetch + streaming Maximum cross-browser compatibility. No CORS required for same origin
Blob URL tracking in preview Leave cleanup to the product Memory leaks are a real problem. The service guarantees cleanup
No global state Centralized file store Files are ephemeral on the front-end. Does not justify a permanent store

4.6 Relationship with other framework modules

Module Relationship
Error Handling The product can use ErrorService to report upload/validation errors. Files does not do it automatically — the error is handled by the component
Environment The product can build upload/download endpoints using EnvironmentService.resolve(). Files does not depend directly on Environment
I18n The error messages in ValidationResult.errors are in English (technical strings). Translation is the product's responsibility
Alerts The product can use AlertService to display success/error toasts after upload/download. Files does not emit alerts
Auth If the upload endpoint requires authentication, the product passes the token via headers in upload(). The global authInterceptor also applies if it is registered
Security Files does not sanitize file contents. If the product displays downloaded HTML content, it must use sanitizeHtml() from the Security module

5. Advanced Usage Patterns

5.1 Validation + upload in a pipeline

async uploadWithValidation(file: File): Promise<void> {
  const result = this.validator.validate(file);
  if (!result.valid) {
    this.alerts.error(result.errors.join(', '));
    return;
  }

  this.uploader.upload(file, '/api/files').subscribe({
    next: (p) => this.progress.set(p),
    error: (err) => this.alerts.error('Upload failed'),
    complete: () => this.alerts.success('File uploaded'),
  });
}

5.2 Picker + validation + upload (complete flow)

async selectAndUpload(): Promise<void> {
  const files = await this.picker.pickFile('application/pdf', true);
  if (files.length === 0) return;

  const valid = files.every(f => this.validator.validate(f).valid);
  if (!valid) {
    this.alerts.error('Some files are invalid');
    return;
  }

  this.uploader.uploadMultiple(files, '/api/files').subscribe({
    next: (progress) => this.multiProgress.set(progress),
    complete: () => this.alerts.success(`${files.length} files uploaded`),
  });
}

5.3 Export + preview before downloading

async previewBeforeExport(data: User[]): Promise<void> {
  const blob = await this.exporter.generateBlob(data, {
    format: 'pdf',
    filename: 'report',
    columns: [
      { key: 'name', header: 'Name' },
      { key: 'email', header: 'Email' },
    ],
  });

  // Preview first
  this.preview.previewInNewTab(blob, 'application/pdf');

  // Then the user decides whether to download
  // this.downloader.downloadBlob(blob, 'report.pdf');
}

5.4 Download from an endpoint with fetch (full control)

async downloadFromApi(fileId: string): Promise<void> {
  const url = this.env.resolve(`/api/files/${fileId}`);

  // If you need control over headers/response
  const response = await fetch(url, {
    headers: { 'Authorization': `Bearer ${this.token}` },
  });
  const blob = await response.blob();
  const filename = response.headers.get('Content-Disposition')
    ?.split('filename=')[1] ?? 'download';

  this.downloader.downloadBlob(blob, filename);
}

5.5 Read the module's active configuration

private readonly config = inject(FILES_CONFIG);

logConfig(): void {
  console.log('Max size:', this.config.maxFileSizeBytes);
  console.log('Allowed types:', this.config.allowedMimeTypes);
  console.log('Max concurrent:', this.config.maxConcurrentUploads);
  console.log('Preview enabled:', this.config.enablePreview);
}

5.6 Cleanup of blob URLs in a component

@Component({ /* ... */ })
export class GalleryComponent implements OnDestroy {
  private readonly preview = inject(FilePreviewService);
  imageUrls: string[] = [];

  addImage(blob: Blob): void {
    this.imageUrls.push(this.preview.createBlobUrl(blob));
  }

  ngOnDestroy(): void {
    this.preview.revokeAll();
    // Or individually:
    // this.imageUrls.forEach(url => this.preview.revokeBlobUrl(url));
  }
}
  • Alerts Module

Clone this wiki locally