Storage service

A dedicated service for managing files.

A service for the purpose of managing files may prove quite useful. The overall structure was inspired by this article. What we'll first do then is to generate a files module and a storage service.

nest g mo files
nest g s files/storage

Here, we'll adopt an ideia similar to the HashingService. That is, to have an abstract class with the methods' signatures and a concrete class with their respective implementations. So, in the StorageService, we should have signatures to:

  • Save a file

  • Create a directory

  • Get a file

  • Get the names of files inside a directory

  • Calculate the amount of files inside a directory

  • Delete a file or directory

  • Validate a path

@Injectable()
export abstract class StorageService {
  abstract saveFile(path: string, file: Express.Multer.File): Promise<void>;
  abstract createDir(path: string): Promise<void>;
  abstract getFile(path: string): StreamableFile;
  abstract getDirFilenames(path: string): Promise<string[]>;
  abstract getDirFilecount(path: string): Promise<number>;
  abstract delete(path: string): Promise<void>;
  abstract validatePath(path: string): Promise<void>;
}

Each method will be better detailed during its implementation. Therefore, let's proceed to implement the concrete class.

Node, by default, has the fs module for managing files. However, the fs-extra package has the same functionalities and more features and improvements. It's already installed, so let's just add its typing.

yarn add -D @types/fs-extra

Now, we can create a concrete service that implements the methods of the StorageService using fs-extra. After creating it, we should make it implement the StorageService.

nest g s files/storage/fse --flat

Let's also already adjust the providers in the FilesModule. Remember to add to the exports, the StorageService.

{
  provide: StorageService,
  useClass: FseService,
},

In file.constants, let's define a constant for the base directory where all the files will be stored, which will be the upload folder at the root of the project.

export const BASE_PATH = 'upload';

Also add this folder in the .gitignore file, as we won't publish files to the code repository.

# Upload
/upload

Let's then, finally, implement each one of the methods in the FseService.

  • saveFile() - Receives the path and the file, and then saves it in this path prepended with the BASE_PATH, with the file's originalname, from its buffer

async saveFile(path: string, file: Express.Multer.File) {
  const fullPath = join(BASE_PATH, path, file.originalname);
  await writeFile(fullPath, file.buffer);
}

The join() function combines many paths into one, in a safe manner.

  • createDir() - The function mkdirp() allows to create folders and even nested ones, avoiding headaches in the future

async createDir(path: string) {
  const fullPath = join(BASE_PATH, path);
  await mkdirp(fullPath);
}

If the path already exists, simply doesn't do anything.

  • getFile() - This function returns the file as a StreamableFile, which allows for more flexibility if desired (further documentation)

getFile(path: string) {
  const fullPath = join(BASE_PATH, path);
  const stream = createReadStream(fullPath);
  return new StreamableFile(stream);
}
  • getDirFilenames() - Obtains the names of files inside a folder

getDirFilenames(path: string) {
  const fullPath = join(BASE_PATH, path);
  return readdir(fullPath);
}
  • getDirFilecount() - Counts how many files are inside a folder using the previous method

async getDirFilecount(path: string) {
  const dirFilenames = await this.getDirFilenames(path);
  return dirFilenames.length;
}

Notice that the fullPath is not obtained here: it is in the previous method.

  • delete() - Deletes a file or folder

async delete(path: string) {
  const fullPath = join(BASE_PATH, path);
  await remove(fullPath);
}

The remove() function also allows for removing folders with contents.

If the path does not exist, simply doesn't do anything.

  • validatePath() - Checks a path, throwing an exception if it does not exist

async validatePath(path: string) {
  const fullPath = join(BASE_PATH, path);
  if (!(await pathExists(fullPath))) {
    throw new NotFoundException('Path not found');
  }
}

And we're done! We now have a quite interesting set of util functions to aid us when managing files.

Commit - Implementing file storage functions

Last updated