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/storageThe 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-extraNow, 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 --flatLet'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
/uploadLet's then, finally, implement each one of the methods in the FseService.
saveFile()- Receives thepathand thefile, and then saves it in thispathprepended 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 apathexists, 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