From d1e9214128f4a907d057b625c11799eaea5d752e Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Mon, 6 Jan 2025 12:38:49 +0100 Subject: [PATCH] fix: move users avatars under the new folder --- .../1735836154221-v-2-2-0.migration.ts | 81 +++++++++++++++++-- api/src/user/controllers/user.controller.ts | 14 +++- api/src/utils/helpers/fs.ts | 69 ++++++++++++++++ 3 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 api/src/utils/helpers/fs.ts diff --git a/api/src/migration/migrations/1735836154221-v-2-2-0.migration.ts b/api/src/migration/migrations/1735836154221-v-2-2-0.migration.ts index ca4886ae..667e0936 100644 --- a/api/src/migration/migrations/1735836154221-v-2-2-0.migration.ts +++ b/api/src/migration/migrations/1735836154221-v-2-2-0.migration.ts @@ -6,8 +6,8 @@ * 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 { existsSync } from 'fs'; +import { join, resolve } from 'path'; import mongoose from 'mongoose'; @@ -16,6 +16,8 @@ import attachmentSchema, { } from '@/attachment/schemas/attachment.schema'; import subscriberSchema, { Subscriber } from '@/chat/schemas/subscriber.schema'; import { config } from '@/config'; +import userSchema, { User } from '@/user/schemas/user.schema'; +import { moveFile, moveFiles } from '@/utils/helpers/fs'; import { MigrationServices } from '../types'; @@ -79,22 +81,85 @@ const unpopulateSubscriberAvatar = async ({ logger }: MigrationServices) => { }; const updateOldAvatarsPath = async ({ logger }: MigrationServices) => { + // Make sure the old folder is moved 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.verbose( + `Moving subscriber avatar files from ${oldPath} to ${config.parameters.avatarDir} ...`, + ); + await moveFiles(oldPath, config.parameters.avatarDir); logger.log('Avatars folder successfully moved to its new location ...'); } else { - logger.log('No old avatars folder found ...'); + logger.log(`No old avatars folder found: ${oldPath}`); + } + + // Move users avatars to the "uploads/avatars" folder + const AttachmentModel = mongoose.model( + Attachment.name, + attachmentSchema, + ); + const UserModel = mongoose.model(User.name, userSchema); + + const cursor = UserModel.find().cursor(); + + for await (const user of cursor) { + try { + if (user.avatar) { + const avatar = await AttachmentModel.findOne({ _id: user.avatar }); + if (avatar) { + const src = resolve( + join(config.parameters.uploadDir, avatar.location), + ); + const dst = resolve( + join(config.parameters.avatarDir, avatar.location), + ); + logger.verbose(`Moving user avatar file from ${src} to ${dst} ...`); + await moveFile(src, dst); + } + } + } catch (err) { + logger.error(err); + logger.error('Unable to move user avatar to the new folder'); + } } }; const restoreOldAvatarsPath = async ({ logger }: MigrationServices) => { + // Move users avatars to the "/app/avatars" folder + const AttachmentModel = mongoose.model( + Attachment.name, + attachmentSchema, + ); + const UserModel = mongoose.model(User.name, userSchema); + + const cursor = UserModel.find().cursor(); + + for await (const user of cursor) { + try { + if (user.avatar) { + const avatar = await AttachmentModel.findOne({ _id: user.avatar }); + if (avatar) { + const src = resolve( + join(config.parameters.avatarDir, avatar.location), + ); + const dst = resolve( + join(config.parameters.uploadDir, avatar.location), + ); + logger.verbose(`Moving user avatar file from ${src} to ${dst} ...`); + await moveFile(src, dst); + } + } + } catch (err) { + logger.error(err); + logger.error('Unable to move user avatar to the new folder'); + } + } + + // 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 ...'); + await moveFiles(config.parameters.avatarDir, oldPath); + logger.log('Avatars folder successfully moved to the old location ...'); } else { logger.log('No avatars folder found ...'); } diff --git a/api/src/user/controllers/user.controller.ts b/api/src/user/controllers/user.controller.ts index 67d23cd6..cc50f821 100644 --- a/api/src/user/controllers/user.controller.ts +++ b/api/src/user/controllers/user.controller.ts @@ -102,14 +102,22 @@ export class ReadOnlyUserController extends BaseController< throw new NotFoundException(`user with ID ${id} not found`); } - if (user.avatar) { + try { + if (!user.avatar) { + throw new Error('User has no avatar'); + } + return await this.attachmentService.download( user.avatar, config.parameters.avatarDir, ); + } catch (err) { + this.logger.verbose( + 'User has no avatar, generating initials avatar ...', + err, + ); + return await generateInitialsAvatar(user); } - - return generateInitialsAvatar(user); } /** diff --git a/api/src/utils/helpers/fs.ts b/api/src/utils/helpers/fs.ts new file mode 100644 index 00000000..c13fe51a --- /dev/null +++ b/api/src/utils/helpers/fs.ts @@ -0,0 +1,69 @@ +/* + * 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 fs from 'fs'; +import { basename, join, resolve } from 'path'; + +export async function moveFile( + sourcePath: string, + destinationPath: string, + overwrite: boolean = true, +): Promise { + // Check if the file exists at the destination + try { + if (overwrite) { + await fs.promises.unlink(destinationPath); // Remove existing file if overwrite is true + } else { + await fs.promises.access(destinationPath); + throw new Error(`File already exists at destination: ${destinationPath}`); + } + } catch { + // Ignore if file does not exist + } + + // Move the file + await fs.promises.copyFile(sourcePath, destinationPath); + await fs.promises.unlink(sourcePath); + return destinationPath; +} + +/** + * Moves all files from a source folder to a destination folder. + * @param sourceFolder - The folder containing the files to move. + * @param destinationFolder - The folder where the files should be moved. + * @param overwrite - Whether to overwrite files if they already exist at the destination (default: false). + * @returns A promise that resolves when all files have been moved. + */ +export async function moveFiles( + sourceFolder: string, + destinationFolder: string, + overwrite: boolean = true, +): Promise { + // Read the contents of the source folder + const files = await fs.promises.readdir(sourceFolder); + + // Filter only files (skip directories) + const filePaths = await Promise.all( + files.map(async (file) => { + const filePath = join(sourceFolder, file); + const stat = await fs.promises.stat(filePath); + return stat.isFile() ? filePath : null; + }), + ); + + // Move each file to the destination folder + const movePromises = filePaths + .filter((filePath): filePath is string => filePath !== null) + .map((filePath) => { + const fileName = basename(filePath); + const destination = resolve(join(destinationFolder, fileName)); + return moveFile(filePath, destination, overwrite); + }); + + await Promise.all(movePromises); +}