Skip to content

Commit

Permalink
refactor(uploadAvatar): employ fileStrategy for avatars, from social …
Browse files Browse the repository at this point in the history
…logins or user upload
  • Loading branch information
danny-avila committed Jan 11, 2024
1 parent eff336b commit 00534ca
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 275 deletions.
9 changes: 7 additions & 2 deletions api/server/routes/files/avatar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const express = require('express');
const multer = require('multer');

const uploadAvatar = require('~/server/services/Files/images/avatar/uploadAvatar');
const uploadAvatar = require('~/server/services/Files/images/avatar');
const { requireJwtAuth } = require('~/server/middleware/');
const User = require('~/models/User');

Expand All @@ -23,7 +23,12 @@ router.post('/', requireJwtAuth, upload.single('input'), async (req, res) => {
if (!user) {
throw new Error('User not found');
}
const url = await uploadAvatar(userId, input, manual);
const url = await uploadAvatar({
input,
userId,
manual,
fileStrategy: req.app.locals.fileStrategy,
});

res.json({ url });
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion api/server/routes/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ const {

const files = require('./files');
const images = require('./images');
const avatar = require('./avatar');

router.use(requireJwtAuth);
router.use(checkBan);
router.use(uaParser);

router.use('/', files);
router.use('/images', images);
router.use('/images/avatar', require('./avatar'));
router.use('/images/avatar', avatar);

module.exports = router;
1 change: 1 addition & 0 deletions api/server/services/AppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const paths = require('~/config/paths');
const AppService = async (app) => {
const config = (await loadCustomConfig()) ?? {};
const fileStrategy = config.fileStrategy ?? FileSources.local;
process.env.CDN_PROVIDER = fileStrategy;

if (fileStrategy === FileSources.firebase) {
initializeFirebase();
Expand Down
40 changes: 38 additions & 2 deletions api/server/services/Files/Firebase/images.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const { saveBufferToFirebase } = require('./crud');
const { resizeImage } = require('../images/resize');
const { saveBufferToFirebase } = require('./crud');
const { updateFile } = require('~/models');
const { logger } = require('~/config');

/**
* Converts an image file to the WebP format. The function first resizes the image based on the specified
Expand Down Expand Up @@ -66,4 +67,39 @@ async function prepareImageURL(req, file) {
return await Promise.all(promises);
}

module.exports = { uploadImageToFirebase, prepareImageURL };
/**
* Uploads a user's avatar to Firebase Storage and returns the URL.
* If the 'manual' flag is set to 'true', it also updates the user's avatar URL in the database.
*
* @param {object} params - The parameters object.
* @param {Buffer} params.buffer - The Buffer containing the avatar image in WebP format.
* @param {object} params.User - The User document (mongoose); TODO: remove direct use of Model, `User`
* @param {string} params.manual - A string flag indicating whether the update is manual ('true' or 'false').
* @returns {Promise<string>} - A promise that resolves with the URL of the uploaded avatar.
* @throws {Error} - Throws an error if Firebase is not initialized or if there is an error in uploading.
*/
async function processFirebaseAvatar({ buffer, User, manual }) {
try {
const downloadURL = await saveBufferToFirebase({
userId: User._id.toString(),
buffer,
fileName: 'avatar.png',
});

const isManual = manual === 'true';

const url = `${downloadURL}?manual=${isManual}`;

if (isManual) {
User.avatar = url;
await User.save();
}

return url;
} catch (error) {
logger.error('Error uploading profile picture:', error);
throw error;
}
}

module.exports = { uploadImageToFirebase, prepareImageURL, processFirebaseAvatar };
44 changes: 43 additions & 1 deletion api/server/services/Files/Local/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,46 @@ async function prepareImagesLocal(req, file) {
return await Promise.all(promises);
}

module.exports = { uploadLocalImage, encodeImage, prepareImagesLocal };
/**
* Uploads a user's avatar to Firebase Storage and returns the URL.
* If the 'manual' flag is set to 'true', it also updates the user's avatar URL in the database.
*
* @param {object} params - The parameters object.
* @param {Buffer} params.buffer - The Buffer containing the avatar image in WebP format.
* @param {object} params.User - The User document (mongoose); TODO: remove direct use of Model, `User`
* @param {string} params.manual - A string flag indicating whether the update is manual ('true' or 'false').
* @returns {Promise<string>} - A promise that resolves with the URL of the uploaded avatar.
* @throws {Error} - Throws an error if Firebase is not initialized or if there is an error in uploading.
*/
async function processLocalAvatar({ buffer, User, manual }) {
const userDir = path.resolve(
__dirname,
'..',
'..',
'..',
'..',
'..',
'client',
'public',
'images',
User._id.toString(),
);
const fileName = `avatar-${new Date().getTime()}.png`;
const urlRoute = `/images/${User._id.toString()}/${fileName}`;
const avatarPath = path.join(userDir, fileName);

await fs.promises.mkdir(userDir, { recursive: true });
await fs.promises.writeFile(avatarPath, buffer);

const isManual = manual === 'true';
let url = `${urlRoute}?manual=${isManual}`;

if (isManual) {
User.avatar = url;
await User.save();
}

return url;
}

module.exports = { uploadLocalImage, encodeImage, prepareImagesLocal, processLocalAvatar };
78 changes: 78 additions & 0 deletions api/server/services/Files/images/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const sharp = require('sharp');
const fs = require('fs').promises;
const fetch = require('node-fetch');
const User = require('~/models/User');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { logger } = require('~/config');

async function convertToWebP(inputBuffer) {
return sharp(inputBuffer).resize({ width: 150 }).toFormat('webp').toBuffer();
}

/**
* Uploads an avatar image for a user. This function can handle various types of input (URL, Buffer, or File object),
* processes the image to a square format, converts it to WebP format, and then uses a specified file strategy for
* further processing. It performs validation on the user ID and the input type. The function can throw errors for
* invalid input types, fetching issues, or other processing errors.
*
* @param {Object} params - The parameters object.
* @param {string} params.userId - The unique identifier of the user for whom the avatar is being uploaded.
* @param {FileSources} params.fileStrategy - The file handling strategy to use, determining how the avatar is processed.
* @param {(string|Buffer|File)} params.input - The input representing the avatar image. Can be a URL (string),
* a Buffer, or a File object.
* @param {string} params.manual - A string flag indicating whether the upload process is manual.
*
* @returns {Promise<any>}
* A promise that resolves to the result of the `processAvatar` function, specific to the chosen file
* strategy. Throws an error if any step in the process fails.
*
* @throws {Error} Throws an error if the user ID is undefined, the input type is invalid, the image fetching fails,
* or any other error occurs during the processing.
*/
async function uploadAvatar({ userId, fileStrategy, input, manual }) {
try {
if (userId === undefined) {
throw new Error('User ID is undefined');
}
const _id = userId;
// TODO: remove direct use of Model, `User`
const oldUser = await User.findOne({ _id });

let imageBuffer;
if (typeof input === 'string') {
const response = await fetch(input);

if (!response.ok) {
throw new Error(`Failed to fetch image from URL. Status: ${response.status}`);
}
imageBuffer = await response.buffer();
} else if (input instanceof Buffer) {
imageBuffer = input;
} else if (typeof input === 'object' && input instanceof File) {
const fileContent = await fs.readFile(input.path);
imageBuffer = Buffer.from(fileContent);
} else {
throw new Error('Invalid input type. Expected URL, Buffer, or File.');
}

const { width, height } = await sharp(imageBuffer).metadata();
const minSize = Math.min(width, height);
const squaredBuffer = await sharp(imageBuffer)
.extract({
left: Math.floor((width - minSize) / 2),
top: Math.floor((height - minSize) / 2),
width: minSize,
height: minSize,
})
.toBuffer();

const webPBuffer = await convertToWebP(squaredBuffer);
const { processAvatar } = getStrategyFunctions(fileStrategy);
return await processAvatar({ buffer: webPBuffer, User: oldUser, manual });
} catch (error) {
logger.error('Error uploading the avatar:', error);
throw error;
}
}

module.exports = uploadAvatar;
29 changes: 0 additions & 29 deletions api/server/services/Files/images/avatar/firebaseStrategy.js

This file was deleted.

32 changes: 0 additions & 32 deletions api/server/services/Files/images/avatar/localStrategy.js

This file was deleted.

63 changes: 0 additions & 63 deletions api/server/services/Files/images/avatar/uploadAvatar.js

This file was deleted.

4 changes: 2 additions & 2 deletions api/server/services/Files/images/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const avatar = require('./avatar');
const encode = require('./encode');
const parse = require('./parse');
const resize = require('./resize');
const validate = require('./validate');
const uploadAvatar = require('./avatar/uploadAvatar');

module.exports = {
...encode,
...parse,
...resize,
...validate,
uploadAvatar,
avatar,
};
Loading

0 comments on commit 00534ca

Please sign in to comment.