🖼️ feat: Avatar GIF Support & Dynamic Extensions (#7657)

This commit is contained in:
Marco Beretta 2025-06-02 13:51:38 +02:00 committed by GitHub
parent aca89091d9
commit 442b149d55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 58 additions and 10 deletions

View file

@ -18,6 +18,7 @@ const {
} = require('~/models/Agent'); } = require('~/models/Agent');
const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process'); const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process');
const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { refreshS3Url } = require('~/server/services/Files/S3/crud'); const { refreshS3Url } = require('~/server/services/Files/S3/crud');
const { updateAction, getActions } = require('~/models/Action'); const { updateAction, getActions } = require('~/models/Action');
const { updateAgentProjects } = require('~/models/Agent'); const { updateAgentProjects } = require('~/models/Agent');
@ -373,12 +374,26 @@ const uploadAgentAvatarHandler = async (req, res) => {
} }
const buffer = await fs.readFile(req.file.path); const buffer = await fs.readFile(req.file.path);
const image = await uploadImageBuffer({
req, const fileStrategy = req.app.locals.fileStrategy;
context: FileContext.avatar,
metadata: { buffer }, const resizedBuffer = await resizeAvatar({
userId: req.user.id,
input: buffer,
}); });
const { processAvatar } = getStrategyFunctions(fileStrategy);
const avatarUrl = await processAvatar({
buffer: resizedBuffer,
userId: req.user.id,
manual: 'false',
});
const image = {
filepath: avatarUrl,
source: fileStrategy,
};
let _avatar; let _avatar;
try { try {
const agent = await getAgent({ id: agent_id }); const agent = await getAgent({ id: agent_id });
@ -403,7 +418,7 @@ const uploadAgentAvatarHandler = async (req, res) => {
const data = { const data = {
avatar: { avatar: {
filepath: image.filepath, filepath: image.filepath,
source: req.app.locals.fileStrategy, source: image.source,
}, },
}; };

View file

@ -97,10 +97,14 @@ async function prepareAzureImageURL(req, file) {
*/ */
async function processAzureAvatar({ buffer, userId, manual, basePath = 'images', containerName }) { async function processAzureAvatar({ buffer, userId, manual, basePath = 'images', containerName }) {
try { try {
const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar.${extension}`;
const downloadURL = await saveBufferToAzure({ const downloadURL = await saveBufferToAzure({
userId, userId,
buffer, buffer,
fileName: 'avatar.png', fileName,
basePath, basePath,
containerName, containerName,
}); });

View file

@ -87,10 +87,14 @@ async function prepareImageURL(req, file) {
*/ */
async function processFirebaseAvatar({ buffer, userId, manual }) { async function processFirebaseAvatar({ buffer, userId, manual }) {
try { try {
const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar.${extension}`;
const downloadURL = await saveBufferToFirebase({ const downloadURL = await saveBufferToFirebase({
userId, userId,
buffer, buffer,
fileName: 'avatar.png', fileName,
}); });
const isManual = manual === 'true'; const isManual = manual === 'true';

View file

@ -129,7 +129,10 @@ async function processLocalAvatar({ buffer, userId, manual }) {
userId, userId,
); );
const fileName = `avatar-${new Date().getTime()}.png`; const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar-${new Date().getTime()}.${extension}`;
const urlRoute = `/images/${userId}/${fileName}`; const urlRoute = `/images/${userId}/${fileName}`;
const avatarPath = path.join(userDir, fileName); const avatarPath = path.join(userDir, fileName);

View file

@ -99,7 +99,11 @@ async function prepareImageURLS3(req, file) {
*/ */
async function processS3Avatar({ buffer, userId, manual, basePath = defaultBasePath }) { async function processS3Avatar({ buffer, userId, manual, basePath = defaultBasePath }) {
try { try {
const downloadURL = await saveBufferToS3({ userId, buffer, fileName: 'avatar.png', basePath }); const metadata = await sharp(buffer).metadata();
const extension = metadata.format === 'gif' ? 'gif' : 'png';
const fileName = `avatar.${extension}`;
const downloadURL = await saveBufferToS3({ userId, buffer, fileName, basePath });
if (manual === 'true') { if (manual === 'true') {
await updateUser(userId, { avatar: downloadURL }); await updateUser(userId, { avatar: downloadURL });
} }

View file

@ -44,8 +44,25 @@ async function resizeAvatar({ userId, input, desiredFormat = EImageOutputType.PN
throw new Error('Invalid input type. Expected URL, Buffer, or File.'); throw new Error('Invalid input type. Expected URL, Buffer, or File.');
} }
const { width, height } = await sharp(imageBuffer).metadata(); const metadata = await sharp(imageBuffer).metadata();
const { width, height } = metadata;
const minSize = Math.min(width, height); const minSize = Math.min(width, height);
if (metadata.format === 'gif') {
const resizedBuffer = await sharp(imageBuffer, { animated: true })
.extract({
left: Math.floor((width - minSize) / 2),
top: Math.floor((height - minSize) / 2),
width: minSize,
height: minSize,
})
.resize(250, 250)
.gif()
.toBuffer();
return resizedBuffer;
}
const squaredBuffer = await sharp(imageBuffer) const squaredBuffer = await sharp(imageBuffer)
.extract({ .extract({
left: Math.floor((width - minSize) / 2), left: Math.floor((width - minSize) / 2),

View file

@ -50,6 +50,7 @@ export const AgentAvatarRender = ({
width="80" width="80"
height="80" height="80"
style={{ opacity: progress < 1 ? 0.4 : 1 }} style={{ opacity: progress < 1 ? 0.4 : 1 }}
key={url || 'default-key'}
/> />
{progress < 1 && ( {progress < 1 && (
<div className="absolute inset-0 flex items-center justify-center bg-black/5 text-white"> <div className="absolute inset-0 flex items-center justify-center bg-black/5 text-white">