Storage service
A dedicated service for managing files.
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.
nest g mo files
nest g s files/storage
The overall structure was inspired by this article from Prateek Kathal (blog).
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: 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 package has the same functionalities and more features/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 the StorageService
to the exports
.
{
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 thepath
and thefile
, and then saves it in thispath
prepended with theBASE_PATH
, with the file'soriginalname
, from itsbuffer
async saveFile(path: string, file: File) {
const fullPath = join(BASE_PATH, path, file.originalname);
await writeFile(fullPath, file.buffer);
}
createDir()
- The functionmkdirp()
allows to create folders and even nested ones
async createDir(path: string) {
const fullPath = join(BASE_PATH, path);
await mkdirp(fullPath);
}
getFile()
- This function returns the file as aStreamableFile
, which allows for more flexibility
getFile(path: string) {
const fullPath = join(BASE_PATH, path);
const stream = createReadStream(fullPath);
return new StreamableFile(stream);
}
This NestJS doc further explains the class mentioned above.
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;
}
delete()
- Deletes a file or folder
async delete(path: string) {
const fullPath = join(BASE_PATH, path);
await remove(fullPath);
}
validatePath()
- Checks if apath
exists, throwing an exception if it does not
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
Before finishing the StorageService
, 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
abstract validateFilecount(count: number, max: number): void;
abstract genUniqueFilename(filename: string): string;
And now, in the FseService
, implement them.
validateFilecount()
- Checks if the file count exceeds a maximum
validateFilecount(count: number, max: number) {
if (count > max) {
throw new ConflictException('File count exceeds max limit');
}
}
genUniqueFilename()
- Prepends a filename with a unique prefix
genUniqueFilename(filename: string) {
const uniquePrefix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
return `${uniquePrefix}-${filename}`;
}
The unique prefix was obtained from the Multer docs.
Lastly, adjust the saveFile()
method to use the unique filename.
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);
}
Commit - Some more features in file storage
Last updated