Fixes - Workspace logo migration (#18035)

- Update migration command to handle case where workspace logo is
originated from workspace email and point to twenty-icons.com
- Update same logic for new workspaces
- Add feature-flag for all newly created workspaces
This commit is contained in:
Etienne
2026-02-18 16:35:48 +01:00
committed by GitHub
parent 3bd431e95d
commit 058489b5cc
11 changed files with 381 additions and 125 deletions
@@ -1,6 +1,7 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import FileType from 'file-type';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FileFolder } from 'twenty-shared/types';
@@ -20,6 +21,7 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
@@ -27,6 +29,7 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { getImageBufferFromUrl } from 'src/utils/image';
@Command({
name: 'upgrade:1-18:migrate-workspace-pictures',
@@ -44,6 +47,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
private readonly fileUrlService: FileUrlService,
private readonly secureHttpClientService: SecureHttpClientService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
@@ -135,14 +139,60 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
return;
}
const isInWorkspaceLogo = workspace.logo.startsWith(
FileFolder.WorkspaceLogo,
);
const isTwentyIconLogo = workspace.logo.includes('twenty-icons');
if (!isTwentyIconLogo && !isInWorkspaceLogo) {
this.logger.log(
`Workspace logo is not a twenty icon or a workspace logo, skipping`,
);
return;
}
this.logger.log(
`Migrating workspace logo for workspace ${workspaceId}: ${workspace.logo}`,
);
if (isInWorkspaceLogo) {
await this.migrateWorkspaceLogoFromWorkspaceFolder({
workspaceId,
logoPath: workspace.logo,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
});
}
if (isTwentyIconLogo) {
await this.migrateWorkspaceLogoFromTwentyIcons({
workspaceId,
logoUrl: workspace.logo,
isDryRun,
workspaceCustomFlatApplication,
});
}
}
private async migrateWorkspaceLogoFromWorkspaceFolder({
workspaceId,
logoPath,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
}: {
workspaceId: string;
logoPath: string;
isDryRun: boolean;
workspaceCustomFlatApplication: FlatApplication;
fileRepository: Repository<FileEntity>;
}): Promise<void> {
try {
const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow(
workspace.logo,
);
const { type: fileExtension } =
extractFolderPathFilenameAndTypeOrThrow(logoPath);
const fileId = v4();
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
@@ -152,7 +202,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: workspace.logo,
filename: logoPath,
},
to: {
folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`,
@@ -181,7 +231,7 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
}
this.logger.log(
`Migrated workspace logo for workspace ${workspaceId} (${workspace.logo} -> ${newResourcePath})`,
`Migrated workspace logo for workspace ${workspaceId} (${logoPath} -> ${newResourcePath})`,
);
} catch (error) {
this.logger.error(
@@ -191,6 +241,67 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
}
}
private async migrateWorkspaceLogoFromTwentyIcons({
workspaceId,
logoUrl,
isDryRun,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
logoUrl: string;
isDryRun: boolean;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
try {
const httpClient = this.secureHttpClientService.getHttpClient();
const buffer = await getImageBufferFromUrl(logoUrl, httpClient);
const type = await FileType.fromBuffer(buffer);
if (!isDefined(type) || !type.mime.startsWith('image/')) {
this.logger.warn(
`Unable to detect image type for workspace logo ${logoUrl}, skipping`,
);
return;
}
const fileId = v4();
const newFilename = `${fileId}.${type.ext}`;
const newResourcePath = `${newFilename}`;
if (!isDryRun) {
const fileEntity = await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
fileFolder: FileFolder.CorePicture,
resourcePath: newResourcePath,
sourceFile: buffer,
mimeType: type.mime,
fileId,
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.workspaceRepository.update(
{ id: workspaceId },
{ logoFileId: fileEntity.id },
);
}
this.logger.log(
`Migrated workspace logo from twenty-icons for workspace ${workspaceId} (${logoUrl} -> ${FileFolder.CorePicture}/${newResourcePath})`,
);
} catch (error) {
this.logger.error(
`Failed to migrate workspace logo from twenty-icons for workspace ${workspaceId}: ${error.message}`,
);
}
}
private async migrateWorkspaceMemberAvatars({
workspaceId,
isDryRun,
@@ -15,6 +15,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -51,6 +52,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
FileModule,
UserWorkspaceModule,
WorkspaceMigrationModule,
SecureHttpClientModule,
],
providers: [
MigratePersonAvatarFilesCommand,