Service logic

Implementing the actual logic for managing the products' images.

Before entering the ProductsService, we should add the FilesModule to the imports of the ProductsModule. This way, the StorageService will be available for injection in its context.

Now, let's actually implement these methods, starting with uploadImages(). Let's begin by simply searching for the entity, but not doing anything with it. This is done just to check its existence.

await this.findOne(id);

We'll use certain path fragments many times, making it useful to have them as constants. So, let's create them in file.constants.

export const FilePath = {
  Products: {
    BASE: 'products',
    IMAGES: 'images',
  },
} as const satisfies Record<string, Record<string, string>>;

After that, back in the service, let's create the path where the images will be saved.

const { BASE, IMAGES } = FilePath.Products;
const path = join(BASE, id.toString(), IMAGES);

Then, as we can save many images, let's ensure that the amount of images that arrived, combined with the amount already present in the directory, don't exceed the limit. However, we should first verify that the folder actually exists, lest an error will be thrown.

if (await pathExists(join(BASE_PATH, path))) {
  const incomingFilecount = files.length;
  const dirFilecount = await this.storageService.getDirFilecount(path);
  const totalFilecount = incomingFilecount + dirFilecount;
  
  this.storageService.validateFilecount(totalFilecount, MaxFileCount.PRODUCT_IMAGES);
}

We checked everything and have the path in hand. Let's then create the directory where the files will be stored, in case it does not exist yet.

await this.storageService.createDir(path);

Finally, let's save the images in parallel using Promise.all().

await Promise.all(
  files.map((file) => this.storageService.saveFile(path, file)),
);

Here, map() is used instead of forEach() due to the latter's unstable behavior with promises.

Now, the downloadImage() method. The step of searching for the entity is identical. However, the path also includes the name of the file to be downloaded.

const path = join(BASE, id.toString(), IMAGES, filename);

Then, it is checked if the file actually exists.

await this.storageService.validatePath(path);

Finally, the file is returned.

return this.storageService.getFile(path);

The last method is deleteImage(), which is practically identical to downloadImage(). The only difference is the last line, where the file is deleted.

await this.storageService.delete(path);

One final improvement: let's make that, when removing a product, its files-related folder is too. First, create an auxiliary method that performs this exclusion.

private async deleteBaseDir(id: number) {
  const { BASE } = FilePath.Products;

  const path = join(BASE, id.toString());
  await this.storageService.delete(path);
}

Then, invoke it in the remove() method.

async remove(id: number) {
  const product = await this.findOne(id);
  await this.productsRepository.remove(product);

  await this.deleteBaseDir(id);

  return product;
}

We have finished implementing the file-related methods in the service.

Commit - Implementing file related methods in service

Last updated