wekan/server/migrations/fixAllFileUrls.js

278 lines
7.6 KiB
JavaScript
Raw Normal View History

/**
* Fix All File URLs Migration
* Ensures all attachment and avatar URLs are universal and work regardless of ROOT_URL and PORT settings
*/
import { ReactiveCache } from '/imports/reactiveCache';
import Users from '/models/users';
import Attachments from '/models/attachments';
import Avatars from '/models/avatars';
import { generateUniversalAttachmentUrl, generateUniversalAvatarUrl, cleanFileUrl, extractFileIdFromUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator';
class FixAllFileUrlsMigration {
constructor() {
this.name = 'fixAllFileUrls';
this.version = 1;
}
/**
* Check if migration is needed
*/
needsMigration() {
// Check for problematic avatar URLs
const users = ReactiveCache.getUsers({});
for (const user of users) {
if (user.profile && user.profile.avatarUrl) {
const avatarUrl = user.profile.avatarUrl;
if (this.hasProblematicUrl(avatarUrl)) {
return true;
}
}
}
// Check for problematic attachment URLs in cards
const cards = ReactiveCache.getCards({});
for (const card of cards) {
if (card.attachments) {
for (const attachment of card.attachments) {
if (attachment.url && this.hasProblematicUrl(attachment.url)) {
return true;
}
}
}
}
return false;
}
/**
* Check if a URL has problematic patterns
*/
hasProblematicUrl(url) {
if (!url) return false;
// Check for auth parameters
if (url.includes('auth=false') || url.includes('brokenIsFine=true')) {
return true;
}
// Check for absolute URLs with domains
if (url.startsWith('http://') || url.startsWith('https://')) {
return true;
}
// Check for ROOT_URL dependencies
if (Meteor.isServer && process.env.ROOT_URL) {
try {
const rootUrl = new URL(process.env.ROOT_URL);
if (rootUrl.pathname && rootUrl.pathname !== '/' && url.includes(rootUrl.pathname)) {
return true;
}
} catch (e) {
// Ignore URL parsing errors
}
}
// Check for non-universal file URLs
if (url.includes('/cfs/files/') && !isUniversalFileUrl(url, 'attachment') && !isUniversalFileUrl(url, 'avatar')) {
return true;
}
return false;
}
/**
* Execute the migration
*/
async execute() {
let filesFixed = 0;
let errors = [];
console.log(`Starting universal file URL migration...`);
try {
// Fix avatar URLs
const avatarFixed = await this.fixAvatarUrls();
filesFixed += avatarFixed;
// Fix attachment URLs
const attachmentFixed = await this.fixAttachmentUrls();
filesFixed += attachmentFixed;
// Fix card attachment references
const cardFixed = await this.fixCardAttachmentUrls();
filesFixed += cardFixed;
} catch (error) {
console.error('Error during file URL migration:', error);
errors.push(error.message);
}
console.log(`Universal file URL migration completed. Fixed ${filesFixed} file URLs.`);
return {
success: errors.length === 0,
filesFixed,
errors
};
}
/**
* Fix avatar URLs in user profiles
*/
async fixAvatarUrls() {
const users = ReactiveCache.getUsers({});
let avatarsFixed = 0;
for (const user of users) {
if (user.profile && user.profile.avatarUrl) {
const avatarUrl = user.profile.avatarUrl;
if (this.hasProblematicUrl(avatarUrl)) {
try {
// Extract file ID from URL
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
let cleanUrl;
if (fileId) {
// Generate universal URL
cleanUrl = generateUniversalAvatarUrl(fileId);
} else {
// Clean existing URL
cleanUrl = cleanFileUrl(avatarUrl, 'avatar');
}
if (cleanUrl && cleanUrl !== avatarUrl) {
// Update user's avatar URL
Users.update(user._id, {
$set: {
'profile.avatarUrl': cleanUrl,
modifiedAt: new Date()
}
});
avatarsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
}
}
} catch (error) {
console.error(`Error fixing avatar URL for user ${user.username}:`, error);
}
}
}
}
return avatarsFixed;
}
/**
* Fix attachment URLs in attachment records
*/
async fixAttachmentUrls() {
const attachments = ReactiveCache.getAttachments({});
let attachmentsFixed = 0;
for (const attachment of attachments) {
// Check if attachment has URL field that needs fixing
if (attachment.url && this.hasProblematicUrl(attachment.url)) {
try {
const fileId = attachment._id;
const cleanUrl = generateUniversalAttachmentUrl(fileId);
if (cleanUrl && cleanUrl !== attachment.url) {
// Update attachment URL
Attachments.update(attachment._id, {
$set: {
url: cleanUrl,
modifiedAt: new Date()
}
});
attachmentsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed attachment URL: ${attachment.url} -> ${cleanUrl}`);
}
}
} catch (error) {
console.error(`Error fixing attachment URL for ${attachment._id}:`, error);
}
}
}
return attachmentsFixed;
}
/**
* Fix attachment URLs in card references
*/
async fixCardAttachmentUrls() {
const cards = ReactiveCache.getCards({});
let cardsFixed = 0;
for (const card of cards) {
if (card.attachments) {
let needsUpdate = false;
const updatedAttachments = card.attachments.map(attachment => {
if (attachment.url && this.hasProblematicUrl(attachment.url)) {
try {
const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment');
const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment');
if (cleanUrl && cleanUrl !== attachment.url) {
needsUpdate = true;
return { ...attachment, url: cleanUrl };
}
} catch (error) {
console.error(`Error fixing card attachment URL:`, error);
}
}
return attachment;
});
if (needsUpdate) {
// Update card with fixed attachment URLs
Cards.update(card._id, {
$set: {
attachments: updatedAttachments,
modifiedAt: new Date()
}
});
cardsFixed++;
if (process.env.DEBUG === 'true') {
console.log(`Fixed attachment URLs in card ${card._id}`);
}
}
}
}
return cardsFixed;
}
}
// Export singleton instance
export const fixAllFileUrlsMigration = new FixAllFileUrlsMigration();
// Meteor methods
Meteor.methods({
'fixAllFileUrls.execute'() {
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
return fixAllFileUrlsMigration.execute();
},
'fixAllFileUrls.needsMigration'() {
if (!this.userId) {
throw new Meteor.Error('not-authorized');
}
return fixAllFileUrlsMigration.needsMigration();
}
});