> For the complete documentation index, see [llms.txt](https://kinesis-school-of-programming.gitbook.io/nestjs-unleashed/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kinesis-school-of-programming.gitbook.io/nestjs-unleashed/extra-module-4-file-management/file-logic/storage-service.md).

# Storage service

A service for the purpose of managing files may prove quite useful. What we'll first do then is to generate a files module and a storage service.

```sh
nest g mo files
nest g s files/storage
```

> The overall structure was inspired by [this article](https://medium.com/the-crowdlinker-chronicle/creating-writing-downloading-files-in-nestjs-ee3e26f2f726) from **Prateek Kathal** (blog).

Here, we'll adopt an ideia similar to the <mark style="color:blue;">`HashingService`</mark>. That is, to have an abstract class with the methods' signatures and a concrete class with their respective implementations. So, in the <mark style="color:blue;">`StorageService`</mark>, 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

```typescript
@Injectable()
export abstract class StorageService {
  abstract saveFile(path: string, file: 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.js, by default, has the **fs** module for managing files. However, the [fs-extra](https://www.npmjs.com/package/fs-extra) package has the same functionalities and more features/improvements. It's already installed, so let's just add its typing.

```sh
yarn add -D @types/fs-extra
```

Now, we can create a concrete service that implements the methods of the <mark style="color:blue;">`StorageService`</mark> using **fs-extra**. After creating it, we should make it implement the <mark style="color:blue;">`StorageService`</mark>.

```sh
nest g s files/storage/fse --flat
```

Let's also already adjust the <mark style="color:blue;">`providers`</mark> in the <mark style="color:blue;">`FilesModule`</mark>. Remember to add the <mark style="color:blue;">`StorageService`</mark> to the <mark style="color:blue;">`exports`</mark>.

```typescript
{
  provide: StorageService,
  useClass: FseService,
},
```

In <mark style="color:purple;">file.constants</mark>, let's define a constant for the base directory where all the files will be stored, which will be the <mark style="color:purple;">upload</mark> folder at the root of the project.

```typescript
export const BASE_PATH = 'upload';
```

Also add this folder in the <mark style="color:purple;">.gitignore</mark> file, as we won't publish files to the code repository.

```gitignore
# Upload
/upload
```

Let's then, finally, implement each one of the methods in the <mark style="color:blue;">`FseService`</mark>.

* <mark style="color:blue;">`saveFile()`</mark> - Receives the <mark style="color:blue;">`path`</mark> and the <mark style="color:blue;">`file`</mark>, and then saves it in this <mark style="color:blue;">`path`</mark> prepended with the <mark style="color:blue;">`BASE_PATH`</mark>, with the file's <mark style="color:blue;">`originalname`</mark>, from its <mark style="color:blue;">`buffer`</mark>

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

{% hint style="info" %}
The <mark style="color:blue;">`join()`</mark> function combines many paths into one safely.
{% endhint %}

* <mark style="color:blue;">`createDir()`</mark> - The function <mark style="color:blue;">`mkdirp()`</mark> allows to create folders and even nested ones

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

{% hint style="info" %}
If the path already exists, it simply doesn't do anything.
{% endhint %}

* <mark style="color:blue;">`getFile()`</mark> - This function returns the file as a <mark style="color:blue;">`StreamableFile`</mark>, which allows for more flexibility

```typescript
getFile(path: string) {
  const fullPath = join(BASE_PATH, path);
  const stream = createReadStream(fullPath);
  return new StreamableFile(stream);
}
```

> This [NestJS doc](https://docs.nestjs.com/techniques/streaming-files) further explains the class mentioned above.

* <mark style="color:blue;">`getDirFilenames()`</mark> - Obtains the names of files inside a folder

```typescript
getDirFilenames(path: string) {
  const fullPath = join(BASE_PATH, path);
  return readdir(fullPath);
}
```

* <mark style="color:blue;">`getDirFilecount()`</mark> - Counts how many files are inside a folder, using the previous method

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

{% hint style="info" %}
Notice that the <mark style="color:blue;">`fullPath`</mark> is not obtained here; it is in the previous method.
{% endhint %}

* <mark style="color:blue;">`delete()`</mark> - Deletes a file or folder

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

{% hint style="info" %}
Some notes:

* The <mark style="color:blue;">`remove()`</mark> function also allows for removing folders with contents

* If the path does not exist, it simply doesn't do anything
  {% endhint %}

* <mark style="color:blue;">`validatePath()`</mark> - Checks if a <mark style="color:blue;">`path`</mark> exists, throwing an exception if it does not

```typescript
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.

<mark style="color:green;">**Commit**</mark> - Implementing file storage functions

Before finishing the <mark style="color:blue;">`StorageService`</mark>, let's just add two more methods there. We may then write methods' signatures to:

* Validate the amount of files to be saved into a directory
* Generate a unique filename from the original one

```typescript
abstract validateFilecount(count: number, max: number): void;
abstract genUniqueFilename(filename: string): string;
```

And now, in the <mark style="color:blue;">`FseService`</mark>, implement them.

* <mark style="color:blue;">`validateFilecount()`</mark> - Checks if the file count exceeds a maximum

```typescript
validateFilecount(count: number, max: number) {
  if (count > max) {
    throw new ConflictException('File count exceeds max limit');
  }
}
```

* <mark style="color:blue;">`genUniqueFilename()`</mark> - Prepends a filename with a unique prefix

```typescript
genUniqueFilename(filename: string) {
  const uniquePrefix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
  return `${uniquePrefix}-${filename}`;
}
```

> The unique prefix was obtained from the [Multer docs](https://github.com/expressjs/multer?tab=readme-ov-file#diskstorage).

{% hint style="info" %}
It's a good practice to save files with unique filenames.
{% endhint %}

Lastly, adjust the <mark style="color:blue;">`saveFile()`</mark> method to use the unique filename.

```typescript
async saveFile(path: string, file: File) {
  const { originalname, buffer } = file;

  const uniqueFilename = this.genUniqueFilename(originalname);
  const fullPath = join(BASE_PATH, path, uniqueFilename);
  await writeFile(fullPath, buffer);
}
```

<mark style="color:green;">**Commit**</mark> - Some more features in file storage
