mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
278 lines
7.6 KiB
JavaScript
278 lines
7.6 KiB
JavaScript
|
|
/**
|
||
|
|
* 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();
|
||
|
|
}
|
||
|
|
});
|