mirror of
https://github.com/hexastack/hexabot
synced 2025-06-04 03:26:22 +00:00
fix: avatar dir
This commit is contained in:
parent
c35be05416
commit
e16660a0a0
2
api/.gitignore
vendored
2
api/.gitignore
vendored
@ -4,4 +4,4 @@ dist/
|
|||||||
coverage/
|
coverage/
|
||||||
uploads/
|
uploads/
|
||||||
documentation/
|
documentation/
|
||||||
avatars
|
avatars
|
||||||
|
@ -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:
|
* 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.
|
* 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).
|
* 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 { MongooseModule } from '@nestjs/mongoose';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
import { AttachmentController } from './controllers/attachment.controller';
|
import { AttachmentController } from './controllers/attachment.controller';
|
||||||
import { AttachmentRepository } from './repositories/attachment.repository';
|
import { AttachmentRepository } from './repositories/attachment.repository';
|
||||||
import { AttachmentModel } from './schemas/attachment.schema';
|
import { AttachmentModel } from './schemas/attachment.schema';
|
||||||
@ -26,4 +30,14 @@ import { AttachmentService } from './services/attachment.service';
|
|||||||
controllers: [AttachmentController],
|
controllers: [AttachmentController],
|
||||||
exports: [AttachmentService],
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs, { createReadStream, promises as fsPromises } from 'fs';
|
import fs, { createReadStream, promises as fsPromises } from 'fs';
|
||||||
import path, { join } from 'path';
|
import { join, resolve } from 'path';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -88,7 +88,9 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
throw new NotFoundException('Profile picture not found');
|
throw new NotFoundException('Profile picture not found');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const path = join(config.parameters.avatarDir, `${foreign_id}.jpeg`);
|
const path = resolve(
|
||||||
|
join(config.parameters.avatarDir, `${foreign_id}.jpeg`),
|
||||||
|
);
|
||||||
if (fs.existsSync(path)) {
|
if (fs.existsSync(path)) {
|
||||||
const picturetream = createReadStream(path);
|
const picturetream = createReadStream(path);
|
||||||
return new StreamableFile(picturetream);
|
return new StreamableFile(picturetream);
|
||||||
@ -129,14 +131,9 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Save profile picture locally
|
// Save profile picture locally
|
||||||
const dirPath = path.join(config.parameters.avatarDir, filename);
|
const dirPath = resolve(join(config.parameters.avatarDir, filename));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure the directory exists
|
|
||||||
await fs.promises.mkdir(config.parameters.avatarDir, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Buffer.isBuffer(data)) {
|
if (Buffer.isBuffer(data)) {
|
||||||
await fs.promises.writeFile(dirPath, data);
|
await fs.promises.writeFile(dirPath, data);
|
||||||
} else {
|
} else {
|
||||||
@ -195,23 +192,25 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
* Otherwise, uploads files to the local directory.
|
* Otherwise, uploads files to the local directory.
|
||||||
*
|
*
|
||||||
* @param file - The file
|
* @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.
|
* @returns A promise that resolves to an array of uploaded attachments.
|
||||||
*/
|
*/
|
||||||
async store(
|
async store(
|
||||||
file: Buffer | Readable | Express.Multer.File,
|
file: Buffer | Readable | Express.Multer.File,
|
||||||
metadata: AttachmentMetadataDto,
|
metadata: AttachmentMetadataDto,
|
||||||
|
rootDir = config.parameters.uploadDir,
|
||||||
): Promise<Attachment> {
|
): Promise<Attachment> {
|
||||||
if (this.getStoragePlugin()) {
|
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);
|
return await this.create(storedDto);
|
||||||
} else {
|
} else {
|
||||||
const dirPath = path.join(config.parameters.uploadDir);
|
|
||||||
const uniqueFilename = generateUniqueFilename(metadata.name);
|
const uniqueFilename = generateUniqueFilename(metadata.name);
|
||||||
const filePath = path.resolve(dirPath, sanitizeFilename(uniqueFilename));
|
const filePath = resolve(join(rootDir, sanitizeFilename(uniqueFilename)));
|
||||||
|
|
||||||
if (!filePath.startsWith(dirPath)) {
|
|
||||||
throw new Error('Invalid file path');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Buffer.isBuffer(file)) {
|
if (Buffer.isBuffer(file)) {
|
||||||
await fsPromises.writeFile(filePath, file);
|
await fsPromises.writeFile(filePath, file);
|
||||||
@ -225,7 +224,7 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
} else {
|
} else {
|
||||||
if (file.path) {
|
if (file.path) {
|
||||||
// For example, if the file is an instance of `Express.Multer.File` (diskStorage case)
|
// 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.copyFile(srcFilePath, filePath);
|
||||||
await fsPromises.unlink(srcFilePath);
|
await fsPromises.unlink(srcFilePath);
|
||||||
} else {
|
} 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({
|
return await this.create({
|
||||||
...metadata,
|
...metadata,
|
||||||
location,
|
location,
|
||||||
@ -255,7 +254,8 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
if (this.getStoragePlugin()) {
|
if (this.getStoragePlugin()) {
|
||||||
return await this.getStoragePlugin().download(attachment);
|
return await this.getStoragePlugin().download(attachment);
|
||||||
} else {
|
} else {
|
||||||
const path = join(rootDir, attachment.location);
|
const path = resolve(join(rootDir, attachment.location));
|
||||||
|
|
||||||
if (!fileExists(path)) {
|
if (!fileExists(path)) {
|
||||||
throw new NotFoundException('No file was found');
|
throw new NotFoundException('No file was found');
|
||||||
}
|
}
|
||||||
@ -289,10 +289,12 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
if (this.getStoragePlugin()) {
|
if (this.getStoragePlugin()) {
|
||||||
return await this.getStoragePlugin().readAsBuffer(attachment);
|
return await this.getStoragePlugin().readAsBuffer(attachment);
|
||||||
} else {
|
} else {
|
||||||
const path = join(rootDir, attachment.location);
|
const path = resolve(join(rootDir, attachment.location));
|
||||||
|
|
||||||
if (!fileExists(path)) {
|
if (!fileExists(path)) {
|
||||||
throw new NotFoundException('No file was found');
|
throw new NotFoundException('No file was found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fs.promises.readFile(path); // Reads the file content as a Buffer
|
return await fs.promises.readFile(path); // Reads the file content as a Buffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
|||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
import { Roles } from '@/utils/decorators/roles.decorator';
|
|
||||||
import { BaseController } from '@/utils/generics/base-controller';
|
import { BaseController } from '@/utils/generics/base-controller';
|
||||||
import { generateInitialsAvatar } from '@/utils/helpers/avatar';
|
import { generateInitialsAvatar } from '@/utils/helpers/avatar';
|
||||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
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.
|
* @param id - The unique identifier of the subscriber whose profile picture is to be retrieved.
|
||||||
* @returns A streamable file containing the avatar image.
|
* @returns A streamable file containing the avatar image.
|
||||||
*/
|
*/
|
||||||
@Roles('public')
|
|
||||||
@Get(':id/profile_pic')
|
@Get(':id/profile_pic')
|
||||||
async getAvatar(@Param('id') id: string): Promise<StreamableFile> {
|
async getAvatar(@Param('id') id: string): Promise<StreamableFile> {
|
||||||
const subscriber = await this.subscriberService.findOneAndPopulate(id);
|
const subscriber = await this.subscriberService.findOneAndPopulate(id);
|
||||||
|
@ -105,12 +105,12 @@ export const config: Config = {
|
|||||||
from: process.env.EMAIL_SMTP_FROM || 'noreply@example.com',
|
from: process.env.EMAIL_SMTP_FROM || 'noreply@example.com',
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
uploadDir: process.env.UPLOAD_DIR
|
uploadDir: join(process.cwd(), process.env.UPLOAD_DIR || '/uploads'),
|
||||||
? join(process.cwd(), process.env.UPLOAD_DIR)
|
avatarDir: join(
|
||||||
: join(process.cwd(), 'uploads'),
|
process.cwd(),
|
||||||
avatarDir: process.env.AVATAR_DIR
|
process.env.UPLOAD_DIR || '/uploads',
|
||||||
? join(process.cwd(), process.env.AVATAR_DIR)
|
'/avatars',
|
||||||
: join(process.cwd(), 'avatars'),
|
),
|
||||||
storageMode: 'disk',
|
storageMode: 'disk',
|
||||||
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
||||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||||
|
@ -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).
|
* 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 mongoose from 'mongoose';
|
||||||
|
|
||||||
import attachmentSchema, {
|
import attachmentSchema, {
|
||||||
Attachment,
|
Attachment,
|
||||||
} from '@/attachment/schemas/attachment.schema';
|
} from '@/attachment/schemas/attachment.schema';
|
||||||
import subscriberSchema, { Subscriber } from '@/chat/schemas/subscriber.schema';
|
import subscriberSchema, { Subscriber } from '@/chat/schemas/subscriber.schema';
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
import { MigrationServices } from '../types';
|
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 = {
|
module.exports = {
|
||||||
async up(services: MigrationServices) {
|
async up(services: MigrationServices) {
|
||||||
await populateSubscriberAvatar(services);
|
await populateSubscriberAvatar(services);
|
||||||
|
await updateOldAvatarsPath(services);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
async down(services: MigrationServices) {
|
async down(services: MigrationServices) {
|
||||||
await unpopulateSubscriberAvatar(services);
|
await unpopulateSubscriberAvatar(services);
|
||||||
|
await restoreOldAvatarsPath(services);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -50,5 +50,6 @@ export abstract class BaseStoragePlugin extends BasePlugin {
|
|||||||
store?(
|
store?(
|
||||||
file: Buffer | Readable | Express.Multer.File,
|
file: Buffer | Readable | Express.Multer.File,
|
||||||
metadata: AttachmentMetadataDto,
|
metadata: AttachmentMetadataDto,
|
||||||
|
rootDir?: string,
|
||||||
): Promise<Attachment>;
|
): Promise<Attachment>;
|
||||||
}
|
}
|
||||||
|
@ -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.
|
* @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')
|
@Get(':id/profile_pic')
|
||||||
async getAvatar(@Param('id') id: string) {
|
async getAvatar(@Param('id') id: string) {
|
||||||
const user = await this.userService.findOneAndPopulate(id);
|
const user = await this.userService.findOneAndPopulate(id);
|
||||||
|
@ -6,6 +6,7 @@ API_PORT=4000
|
|||||||
APP_FRONTEND_PORT=8080
|
APP_FRONTEND_PORT=8080
|
||||||
APP_SCRIPT_COMPODOC_PORT=9003
|
APP_SCRIPT_COMPODOC_PORT=9003
|
||||||
API_ORIGIN=http://${APP_DOMAIN}:${API_PORT}
|
API_ORIGIN=http://${APP_DOMAIN}:${API_PORT}
|
||||||
|
# Specifies if the current instance has primary write (used in DB Migrations)
|
||||||
API_IS_PRIMARY_NODE=true
|
API_IS_PRIMARY_NODE=true
|
||||||
FRONTEND_BASE_URL=http://${APP_DOMAIN}:${APP_FRONTEND_PORT}
|
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}
|
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
|
HTTPS_ENABLED=false
|
||||||
SESSION_SECRET=f661ff500fff6b0c8f91310b6fff6b0c
|
SESSION_SECRET=f661ff500fff6b0c8f91310b6fff6b0c
|
||||||
SESSION_NAME=s.id
|
SESSION_NAME=s.id
|
||||||
|
# Relative attachments upload directory path to the app folder
|
||||||
UPLOAD_DIR=/uploads
|
UPLOAD_DIR=/uploads
|
||||||
AVATAR_DIR=/avatars
|
# Max attachments upload size in bytes
|
||||||
UPLOAD_MAX_SIZE_IN_BYTES=20971520
|
UPLOAD_MAX_SIZE_IN_BYTES=20971520
|
||||||
INVITATION_JWT_SECRET=dev_only
|
INVITATION_JWT_SECRET=dev_only
|
||||||
INVITATION_EXPIRES_IN=24h
|
INVITATION_EXPIRES_IN=24h
|
||||||
@ -24,7 +26,9 @@ PASSWORD_RESET_JWT_SECRET=dev_only
|
|||||||
PASSWORD_RESET_EXPIRES_IN=1h
|
PASSWORD_RESET_EXPIRES_IN=1h
|
||||||
CONFIRM_ACCOUNT_SECRET=dev_only
|
CONFIRM_ACCOUNT_SECRET=dev_only
|
||||||
CONFIRM_ACCOUNT_EXPIRES_IN=1h
|
CONFIRM_ACCOUNT_EXPIRES_IN=1h
|
||||||
|
# Public attachments download URLs JWT Sign secret
|
||||||
SIGNED_URL_SECRET=dev_only
|
SIGNED_URL_SECRET=dev_only
|
||||||
|
# Public attachments download URLs download expiration
|
||||||
SIGNED_URL_EXPIRES_IN=1h
|
SIGNED_URL_EXPIRES_IN=1h
|
||||||
I18N_TRANSLATION_FILENAME=messages
|
I18N_TRANSLATION_FILENAME=messages
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ services:
|
|||||||
- app-network
|
- app-network
|
||||||
volumes:
|
volumes:
|
||||||
- api-data:/app/uploads
|
- api-data:/app/uploads
|
||||||
- api-avatars-data:/app/avatars
|
|
||||||
depends_on:
|
depends_on:
|
||||||
mongo:
|
mongo:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@ -56,7 +55,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
mongo-data:
|
mongo-data:
|
||||||
api-data:
|
api-data:
|
||||||
api-avatars-data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
db-network:
|
db-network:
|
||||||
|
Loading…
Reference in New Issue
Block a user