fix: avatar dir

This commit is contained in:
Mohamed Marrouchi 2025-01-05 08:52:36 +01:00
parent c35be05416
commit e16660a0a0
10 changed files with 79 additions and 35 deletions

2
api/.gitignore vendored
View File

@ -4,4 +4,4 @@ dist/
coverage/
uploads/
documentation/
avatars
avatars

View File

@ -1,15 +1,19 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { Module } from '@nestjs/common';
import { existsSync, mkdirSync } from 'fs';
import { Module, OnApplicationBootstrap } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { config } from '@/config';
import { AttachmentController } from './controllers/attachment.controller';
import { AttachmentRepository } from './repositories/attachment.repository';
import { AttachmentModel } from './schemas/attachment.schema';
@ -26,4 +30,14 @@ import { AttachmentService } from './services/attachment.service';
controllers: [AttachmentController],
exports: [AttachmentService],
})
export class AttachmentModule {}
export class AttachmentModule implements OnApplicationBootstrap {
onApplicationBootstrap() {
// Ensure the directories exists
if (!existsSync(config.parameters.uploadDir)) {
mkdirSync(config.parameters.uploadDir, { recursive: true });
}
if (!existsSync(config.parameters.avatarDir)) {
mkdirSync(config.parameters.avatarDir, { recursive: true });
}
}
}

View File

@ -7,7 +7,7 @@
*/
import fs, { createReadStream, promises as fsPromises } from 'fs';
import path, { join } from 'path';
import { join, resolve } from 'path';
import { Readable } from 'stream';
import {
@ -88,7 +88,9 @@ export class AttachmentService extends BaseService<Attachment> {
throw new NotFoundException('Profile picture not found');
}
} else {
const path = join(config.parameters.avatarDir, `${foreign_id}.jpeg`);
const path = resolve(
join(config.parameters.avatarDir, `${foreign_id}.jpeg`),
);
if (fs.existsSync(path)) {
const picturetream = createReadStream(path);
return new StreamableFile(picturetream);
@ -129,14 +131,9 @@ export class AttachmentService extends BaseService<Attachment> {
}
} else {
// Save profile picture locally
const dirPath = path.join(config.parameters.avatarDir, filename);
const dirPath = resolve(join(config.parameters.avatarDir, filename));
try {
// Ensure the directory exists
await fs.promises.mkdir(config.parameters.avatarDir, {
recursive: true,
});
if (Buffer.isBuffer(data)) {
await fs.promises.writeFile(dirPath, data);
} else {
@ -195,23 +192,25 @@ export class AttachmentService extends BaseService<Attachment> {
* Otherwise, uploads files to the local directory.
*
* @param file - The file
* @param metadata - The attachment metadata informations.
* @param rootDir - The root directory where attachment shoud be located.
* @returns A promise that resolves to an array of uploaded attachments.
*/
async store(
file: Buffer | Readable | Express.Multer.File,
metadata: AttachmentMetadataDto,
rootDir = config.parameters.uploadDir,
): Promise<Attachment> {
if (this.getStoragePlugin()) {
const storedDto = await this.getStoragePlugin().store(file, metadata);
const storedDto = await this.getStoragePlugin().store(
file,
metadata,
rootDir,
);
return await this.create(storedDto);
} else {
const dirPath = path.join(config.parameters.uploadDir);
const uniqueFilename = generateUniqueFilename(metadata.name);
const filePath = path.resolve(dirPath, sanitizeFilename(uniqueFilename));
if (!filePath.startsWith(dirPath)) {
throw new Error('Invalid file path');
}
const filePath = resolve(join(rootDir, sanitizeFilename(uniqueFilename)));
if (Buffer.isBuffer(file)) {
await fsPromises.writeFile(filePath, file);
@ -225,7 +224,7 @@ export class AttachmentService extends BaseService<Attachment> {
} else {
if (file.path) {
// For example, if the file is an instance of `Express.Multer.File` (diskStorage case)
const srcFilePath = path.resolve(file.path);
const srcFilePath = resolve(file.path);
await fsPromises.copyFile(srcFilePath, filePath);
await fsPromises.unlink(srcFilePath);
} else {
@ -233,7 +232,7 @@ export class AttachmentService extends BaseService<Attachment> {
}
}
const location = filePath.replace(dirPath, '');
const location = filePath.replace(rootDir, '');
return await this.create({
...metadata,
location,
@ -255,7 +254,8 @@ export class AttachmentService extends BaseService<Attachment> {
if (this.getStoragePlugin()) {
return await this.getStoragePlugin().download(attachment);
} else {
const path = join(rootDir, attachment.location);
const path = resolve(join(rootDir, attachment.location));
if (!fileExists(path)) {
throw new NotFoundException('No file was found');
}
@ -289,10 +289,12 @@ export class AttachmentService extends BaseService<Attachment> {
if (this.getStoragePlugin()) {
return await this.getStoragePlugin().readAsBuffer(attachment);
} else {
const path = join(rootDir, attachment.location);
const path = resolve(join(rootDir, attachment.location));
if (!fileExists(path)) {
throw new NotFoundException('No file was found');
}
return await fs.promises.readFile(path); // Reads the file content as a Buffer
}
}

View File

@ -23,7 +23,6 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import { config } from '@/config';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { Roles } from '@/utils/decorators/roles.decorator';
import { BaseController } from '@/utils/generics/base-controller';
import { generateInitialsAvatar } from '@/utils/helpers/avatar';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
@ -145,7 +144,6 @@ export class SubscriberController extends BaseController<
* @param id - The unique identifier of the subscriber whose profile picture is to be retrieved.
* @returns A streamable file containing the avatar image.
*/
@Roles('public')
@Get(':id/profile_pic')
async getAvatar(@Param('id') id: string): Promise<StreamableFile> {
const subscriber = await this.subscriberService.findOneAndPopulate(id);

View File

@ -105,12 +105,12 @@ export const config: Config = {
from: process.env.EMAIL_SMTP_FROM || 'noreply@example.com',
},
parameters: {
uploadDir: process.env.UPLOAD_DIR
? join(process.cwd(), process.env.UPLOAD_DIR)
: join(process.cwd(), 'uploads'),
avatarDir: process.env.AVATAR_DIR
? join(process.cwd(), process.env.AVATAR_DIR)
: join(process.cwd(), 'avatars'),
uploadDir: join(process.cwd(), process.env.UPLOAD_DIR || '/uploads'),
avatarDir: join(
process.cwd(),
process.env.UPLOAD_DIR || '/uploads',
'/avatars',
),
storageMode: 'disk',
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)

View File

@ -6,12 +6,16 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { existsSync, promises as fsPromises } from 'fs';
import { join } from 'path';
import mongoose from 'mongoose';
import attachmentSchema, {
Attachment,
} from '@/attachment/schemas/attachment.schema';
import subscriberSchema, { Subscriber } from '@/chat/schemas/subscriber.schema';
import { config } from '@/config';
import { MigrationServices } from '../types';
@ -74,13 +78,37 @@ const unpopulateSubscriberAvatar = async ({ logger }: MigrationServices) => {
}
};
const updateOldAvatarsPath = async ({ logger }: MigrationServices) => {
const oldPath = join(process.cwd(), process.env.AVATAR_DIR || '/avatars');
if (existsSync(oldPath)) {
await fsPromises.copyFile(oldPath, config.parameters.avatarDir);
await fsPromises.unlink(oldPath);
logger.log('Avatars folder successfully moved to its new location ...');
} else {
logger.log('No old avatars folder found ...');
}
};
const restoreOldAvatarsPath = async ({ logger }: MigrationServices) => {
const oldPath = join(process.cwd(), process.env.AVATAR_DIR || '/avatars');
if (existsSync(config.parameters.avatarDir)) {
await fsPromises.copyFile(config.parameters.avatarDir, oldPath);
await fsPromises.unlink(config.parameters.avatarDir);
logger.log('Avatars folder successfully moved to its old location ...');
} else {
logger.log('No avatars folder found ...');
}
};
module.exports = {
async up(services: MigrationServices) {
await populateSubscriberAvatar(services);
await updateOldAvatarsPath(services);
return true;
},
async down(services: MigrationServices) {
await unpopulateSubscriberAvatar(services);
await restoreOldAvatarsPath(services);
return true;
},
};

View File

@ -50,5 +50,6 @@ export abstract class BaseStoragePlugin extends BasePlugin {
store?(
file: Buffer | Readable | Express.Multer.File,
metadata: AttachmentMetadataDto,
rootDir?: string,
): Promise<Attachment>;
}

View File

@ -95,7 +95,6 @@ export class ReadOnlyUserController extends BaseController<
*
* @returns A promise that resolves to the user's avatar or an avatar generated from initials if not found.
*/
@Roles('public')
@Get(':id/profile_pic')
async getAvatar(@Param('id') id: string) {
const user = await this.userService.findOneAndPopulate(id);

View File

@ -6,6 +6,7 @@ API_PORT=4000
APP_FRONTEND_PORT=8080
APP_SCRIPT_COMPODOC_PORT=9003
API_ORIGIN=http://${APP_DOMAIN}:${API_PORT}
# Specifies if the current instance has primary write (used in DB Migrations)
API_IS_PRIMARY_NODE=true
FRONTEND_BASE_URL=http://${APP_DOMAIN}:${APP_FRONTEND_PORT}
FRONTEND_ORIGIN=${FRONTEND_BASE_URL},http://${APP_DOMAIN}:8081,http://${APP_DOMAIN}:5173,http://${APP_DOMAIN},https://${APP_DOMAIN}
@ -15,8 +16,9 @@ SALT_LENGTH=12
HTTPS_ENABLED=false
SESSION_SECRET=f661ff500fff6b0c8f91310b6fff6b0c
SESSION_NAME=s.id
# Relative attachments upload directory path to the app folder
UPLOAD_DIR=/uploads
AVATAR_DIR=/avatars
# Max attachments upload size in bytes
UPLOAD_MAX_SIZE_IN_BYTES=20971520
INVITATION_JWT_SECRET=dev_only
INVITATION_EXPIRES_IN=24h
@ -24,7 +26,9 @@ PASSWORD_RESET_JWT_SECRET=dev_only
PASSWORD_RESET_EXPIRES_IN=1h
CONFIRM_ACCOUNT_SECRET=dev_only
CONFIRM_ACCOUNT_EXPIRES_IN=1h
# Public attachments download URLs JWT Sign secret
SIGNED_URL_SECRET=dev_only
# Public attachments download URLs download expiration
SIGNED_URL_EXPIRES_IN=1h
I18N_TRANSLATION_FILENAME=messages

View File

@ -12,7 +12,6 @@ services:
- app-network
volumes:
- api-data:/app/uploads
- api-avatars-data:/app/avatars
depends_on:
mongo:
condition: service_healthy
@ -56,7 +55,6 @@ services:
volumes:
mongo-data:
api-data:
api-avatars-data:
networks:
db-network: