-
Notifications
You must be signed in to change notification settings - Fork 5.1k
File - Migrate avatarUrl > avatarFile on person (data migration + logic) + Attachment data migration #17752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Greptile OverviewGreptile SummaryThis PR migrates person avatar storage from the legacy Key Changes:
Critical Issues Found:
Implementation Notes:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Admin as Admin/System
participant MigCmd as Migration Command
participant FeatureFlag as Feature Flag Service
participant FileStorage as File Storage
participant DB as Database (Core + Workspace)
participant FE as Frontend (React)
participant User as End User
Note over Admin,DB: Migration Phase (Backend)
Admin->>MigCmd: Run upgrade:1-18:migrate-person-avatar-files
MigCmd->>FeatureFlag: Check IS_FILES_FIELD_MIGRATED
alt Already Migrated
FeatureFlag-->>MigCmd: true
MigCmd-->>Admin: Skip (already completed)
else Not Migrated
FeatureFlag-->>MigCmd: false
MigCmd->>DB: Check/Create avatarFile field metadata
MigCmd->>DB: Fetch persons with avatarUrl (ILike PersonPicture)
loop For each person
MigCmd->>FileStorage: Copy file from old path to new path
MigCmd->>DB: Create FileEntity record (size=0, isTemporaryFile=true)
MigCmd->>DB: Update person.avatarFile with fileId
end
MigCmd->>FeatureFlag: Enable IS_FILES_FIELD_MIGRATED
MigCmd-->>Admin: Migration complete
end
Note over FE,User: Runtime Phase (Frontend)
User->>FE: View person record
FE->>FeatureFlag: Check IS_FILES_FIELD_MIGRATED
alt Files Field Migrated
FeatureFlag-->>FE: true
FE->>FE: Use avatarFile[0].url
FE->>User: Display avatar from new field
else Not Migrated
FeatureFlag-->>FE: false
FE->>FE: Use avatarUrl with getImageAbsoluteURI
FE->>User: Display avatar from legacy field
end
Note over FE,DB: Upload Phase (Frontend)
User->>FE: Upload new avatar
FE->>FeatureFlag: Check IS_FILES_FIELD_MIGRATED
alt Files Field Migrated
FeatureFlag-->>FE: true
FE->>DB: uploadFilesFieldFile mutation
DB-->>FE: FileEntity with id
FE->>DB: Update person.avatarFile
else Not Migrated
FeatureFlag-->>FE: false
FE->>DB: uploadImage mutation (PersonPicture folder)
DB-->>FE: Signed file path
FE->>DB: Update person.avatarUrl
end
FE-->>User: Avatar updated
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4 files reviewed, 3 comments
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Outdated
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Fixed
Show fixed
Hide fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 issue found across 32 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts">
<violation number="1" location="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts:251">
P1: Migrated avatar files should be marked as permanent (`isTemporaryFile: false`). These files are being migrated from an existing permanent storage location, not uploaded as new temporary files awaiting confirmation. Setting `isTemporaryFile: true` could cause data loss if there's a cleanup process that removes temporary files.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:19839 This environment will automatically shut down when the PR is closed or after 5 hours. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts">
<violation number="1" location="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts:252">
P1: Migrated files are marked as `isTemporaryFile: true`, but these should be permanent files. Temporary file cleanup jobs could inadvertently delete these migrated attachments. Consider setting `isTemporaryFile: false` for permanently migrated data.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| applicationId: twentyStandardApplication.id, | ||
| size: -1, | ||
| settings: { | ||
| isTemporaryFile: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Migrated files are marked as isTemporaryFile: true, but these should be permanent files. Temporary file cleanup jobs could inadvertently delete these migrated attachments. Consider setting isTemporaryFile: false for permanently migrated data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts, line 252:
<comment>Migrated files are marked as `isTemporaryFile: true`, but these should be permanent files. Temporary file cleanup jobs could inadvertently delete these migrated attachments. Consider setting `isTemporaryFile: false` for permanently migrated data.</comment>
<file context>
@@ -0,0 +1,287 @@
+ applicationId: twentyStandardApplication.id,
+ size: -1,
+ settings: {
+ isTemporaryFile: true,
+ toDelete: false,
+ },
</file context>
2316f4b to
0b6265a
Compare
| const isMigrated = await this.featureFlagService.isFeatureEnabled( | ||
| FeatureFlagKey.IS_FILES_FIELD_MIGRATED, | ||
| workspaceId, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The migrateAttachmentFilesCommand migration script checks for the IS_FILES_FIELD_MIGRATED feature flag but does not set it upon completion, unlike the migratePersonAvatarFilesCommand.
Severity: HIGH
Suggested Fix
Add logic to the end of the migrateAttachmentFilesCommand to set the IS_FILES_FIELD_MIGRATED feature flag to true upon successful completion, mirroring the implementation in migratePersonAvatarFilesCommand. This ensures the migration's state is correctly tracked.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts#L59-L62
Potential issue: The `migrateAttachmentFilesCommand` migration script checks for the
`IS_FILES_FIELD_MIGRATED` feature flag to ensure it only runs once, but it does not set
this flag upon completion. The subsequent `migratePersonAvatarFilesCommand` does set the
flag. If the migration process fails after the attachment migration but before the
person avatar migration completes, or if the execution order is changed, the attachment
migration could be skipped on a re-run, leading to an incomplete data migration. This
inconsistency makes the migration process brittle and not truly idempotent.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there first step review here
export const extractFileInfo = async ({
file,
declaredMimeType,
filename,
}: {
file: Buffer;
declaredMimeType: string | undefined;
filename: string;
}) => {
const { ext: declaredExt } = buildFileInfo(filename);
const detectedFileType = await FileType.fromBuffer(file);
const mimeType = detectedFileType?.mime ?? declaredMimeType;
const ext = detectedFileType?.ext ?? declaredExt;
return {
mimeType,
ext,
};
};I'm not sure about this here, I wouldn't never trust the declared client data on this. And at the same time I don't know how it would behave if you upload a markdown mime type file but as txt
Unless I'm mistaken there's a possibility to insert a file with a mimetype and file ext fully provided by the client
Maybe never trust the mime type but do for the extension as several can match a single mime type ?
We might wanna dig that a little bit further on what's done usually
EDIT claude response
Great question — this is a nuanced topic. Here's my take:
The core issue
FileType.fromBuffer (magic bytes detection) is the gold standard for binary files (images, PDFs, archives, executables, etc.), but it returns undefined for all text-based formats — .txt, .md, .csv, .json, .html, .css, .yaml, etc. — because these don't have distinctive magic byte signatures.
So the current fallback logic means:
- Binary files: Detected type wins — good, secure.
- Text files: Falls back entirely to client-declared data — that's the risky part.
The dangerous scenario
Someone uploads a text file but declares Content-Type: application/pdf or even application/javascript. Since fromBuffer returns undefined, the code blindly trusts the declared MIME type. Depending on how the file is later served (e.g., Content-Type header on download), this could lead to MIME confusion or XSS if served inline.
The markdown-as-txt case
Not really a security concern. Both are text formats, fromBuffer returns undefined for both, so you'd get the declared values. The worst that happens is a .md file being served as text/plain — harmless.
Industry best practice
The common approach is a three-tier strategy:
- Prefer magic byte detection when available (binary files).
- When detection fails, derive MIME type from the extension using a trusted server-side mapping (e.g., the
mime-typesnpm package), rather than trusting the client's declared MIME type. - Fall back to the declared extension — this is more acceptable because extensions are less security-sensitive than MIME types (MIME types control how browsers interpret content).
Something like:
import mime from 'mime-types';
export const extractFileInfo = async ({
file,
declaredMimeType,
filename,
}: {
file: Buffer;
declaredMimeType: string | undefined;
filename: string;
}) => {
const { ext: declaredExt } = buildFileInfo(filename);
const detectedFileType = await FileType.fromBuffer(file);
// For extension: prefer detected, fall back to declared (acceptable)
const ext = detectedFileType?.ext ?? declaredExt;
// For MIME type: prefer detected, then derive from extension,
// only trust declared as last resort
const mimeType =
detectedFileType?.mime ?? mime.lookup(ext) || declaredMimeType;
return { mimeType, ext };
};TL;DR — The reviewer's instinct is right
- Never trust the declared MIME type as a primary source. Derive it from the extension (via a trusted mapping) when magic bytes fail.
- The declared extension is okay as a fallback — it's less security-critical, and as you noted, multiple extensions can map to one MIME type so the client's choice is reasonable.
- An additional layer of defense would be a whitelist of allowed MIME types depending on the upload context, but that's a separate concern.
...es/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerActions.ts
Show resolved
Hide resolved
packages/twenty-front/src/modules/object-record/record-show/hooks/usePersonAvatarUpload.ts
Show resolved
Hide resolved
packages/twenty-front/src/modules/object-record/record-show/hooks/usePersonAvatarUpload.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-modules/file/files-field/files-field.service.ts
Show resolved
Hide resolved
...wenty-server/src/engine/workspace-manager/workspace-migration/constant/standard-field-ids.ts
Show resolved
Hide resolved
prastoin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice job ! Solid
Left few comments on the upgrade commands ! Please let me know
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
...c/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts
Show resolved
Hide resolved
| assertIsDefinedOrThrow(person.avatarUrl); | ||
|
|
||
| try { | ||
| const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Have you checked all production occurrences in order to search for any some edge cases ? ( such as no extension etc )
Migration command
Check IS_FILES_FIELD_MIGRATED:false
Check or create avatarFile field
Fetch all people with avatarUrl
- Move (Copy/move) file in storage
- Create core.file record
- Update person record
bonus : attachment migration : fullPath > file (same logic)
BE logic
FE logic
The whole imageIdentifier logic will be done later