# 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://kinesis-school-of-programming.gitbook.io/nestjs-unleashed/extra-module-4-file-management/file-logic/storage-service.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
