@solidxai/core 0.1.12-beta.0 → 0.1.12-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/seed.command.d.ts.map +1 -1
- package/dist/commands/seed.command.js +10 -2
- package/dist/commands/seed.command.js.map +1 -1
- package/dist/constants/error-messages.d.ts +1 -0
- package/dist/constants/error-messages.d.ts.map +1 -1
- package/dist/constants/error-messages.js +1 -0
- package/dist/constants/error-messages.js.map +1 -1
- package/dist/constants/media-file-types.d.ts +5 -0
- package/dist/constants/media-file-types.d.ts.map +1 -0
- package/dist/constants/media-file-types.js +27 -0
- package/dist/constants/media-file-types.js.map +1 -0
- package/dist/controllers/media.controller.d.ts +14 -11
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +72 -33
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/dtos/create-chatter-message.dto.d.ts +1 -0
- package/dist/dtos/create-chatter-message.dto.d.ts.map +1 -1
- package/dist/dtos/create-chatter-message.dto.js +7 -1
- package/dist/dtos/create-chatter-message.dto.js.map +1 -1
- package/dist/dtos/create-media.dto.d.ts +1 -0
- package/dist/dtos/create-media.dto.d.ts.map +1 -1
- package/dist/dtos/create-media.dto.js +7 -1
- package/dist/dtos/create-media.dto.js.map +1 -1
- package/dist/dtos/create-role-metadata.dto.d.ts +2 -0
- package/dist/dtos/create-role-metadata.dto.d.ts.map +1 -1
- package/dist/dtos/create-role-metadata.dto.js +3 -1
- package/dist/dtos/create-role-metadata.dto.js.map +1 -1
- package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
- package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
- package/dist/dtos/post-chatter-message.dto.js +6 -1
- package/dist/dtos/post-chatter-message.dto.js.map +1 -1
- package/dist/dtos/update-chatter-message.dto.d.ts +1 -0
- package/dist/dtos/update-chatter-message.dto.d.ts.map +1 -1
- package/dist/dtos/update-chatter-message.dto.js +8 -1
- package/dist/dtos/update-chatter-message.dto.js.map +1 -1
- package/dist/dtos/update-chatter-note-message.dto.d.ts +1 -0
- package/dist/dtos/update-chatter-note-message.dto.d.ts.map +1 -1
- package/dist/dtos/update-chatter-note-message.dto.js +7 -1
- package/dist/dtos/update-chatter-note-message.dto.js.map +1 -1
- package/dist/dtos/update-media.dto.d.ts +1 -0
- package/dist/dtos/update-media.dto.d.ts.map +1 -1
- package/dist/dtos/update-media.dto.js +7 -1
- package/dist/dtos/update-media.dto.js.map +1 -1
- package/dist/entities/chatter-message.entity.d.ts +1 -0
- package/dist/entities/chatter-message.entity.d.ts.map +1 -1
- package/dist/entities/chatter-message.entity.js +5 -1
- package/dist/entities/chatter-message.entity.js.map +1 -1
- package/dist/entities/media.entity.js.map +1 -1
- package/dist/enums/auth-type.enum.d.ts +2 -1
- package/dist/enums/auth-type.enum.d.ts.map +1 -1
- package/dist/enums/auth-type.enum.js +1 -0
- package/dist/enums/auth-type.enum.js.map +1 -1
- package/dist/guards/authentication.guard.d.ts +3 -1
- package/dist/guards/authentication.guard.d.ts.map +1 -1
- package/dist/guards/authentication.guard.js +5 -1
- package/dist/guards/authentication.guard.js.map +1 -1
- package/dist/guards/media-signed-url.guard.d.ts +9 -0
- package/dist/guards/media-signed-url.guard.d.ts.map +1 -0
- package/dist/guards/media-signed-url.guard.js +49 -0
- package/dist/guards/media-signed-url.guard.js.map +1 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +10 -13
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- package/dist/helpers/solid-core-error-codes-provider.service.d.ts.map +1 -1
- package/dist/helpers/solid-core-error-codes-provider.service.js +9 -0
- package/dist/helpers/solid-core-error-codes-provider.service.js.map +1 -1
- package/dist/interfaces/chatter-mention-notification.interface.d.ts +27 -0
- package/dist/interfaces/chatter-mention-notification.interface.d.ts.map +1 -0
- package/dist/interfaces/chatter-mention-notification.interface.js +3 -0
- package/dist/interfaces/chatter-mention-notification.interface.js.map +1 -0
- package/dist/interfaces.d.ts +19 -1
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.js.map +1 -1
- package/dist/jobs/chatter-mention-notification-email-queue-handler.service.d.ts +17 -0
- package/dist/jobs/chatter-mention-notification-email-queue-handler.service.d.ts.map +1 -0
- package/dist/jobs/chatter-mention-notification-email-queue-handler.service.js +106 -0
- package/dist/jobs/chatter-mention-notification-email-queue-handler.service.js.map +1 -0
- package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.d.ts +12 -0
- package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.d.ts.map +1 -0
- package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.js +39 -0
- package/dist/jobs/database/chatter-mention-notification-email-publisher-database.service.js.map +1 -0
- package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.d.ts +8 -0
- package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.d.ts.map +1 -0
- package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.js +10 -0
- package/dist/jobs/database/chatter-mention-notification-email-queue-options-database.js.map +1 -0
- package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.d.ts +20 -0
- package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.d.ts.map +1 -0
- package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.js +48 -0
- package/dist/jobs/database/chatter-mention-notification-email-subscriber-database.service.js.map +1 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.d.ts +12 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.d.ts.map +1 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.js +39 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.js.map +1 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.d.ts +8 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.d.ts.map +1 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.js +10 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-queue-options.js.map +1 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.d.ts +18 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.d.ts.map +1 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.js +45 -0
- package/dist/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.js.map +1 -0
- package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.d.ts +12 -0
- package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.d.ts.map +1 -0
- package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.js +39 -0
- package/dist/jobs/redis/chatter-mention-notification-email-publisher-redis.service.js.map +1 -0
- package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.d.ts +8 -0
- package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.d.ts.map +1 -0
- package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.js +10 -0
- package/dist/jobs/redis/chatter-mention-notification-email-queue-options-redis.js.map +1 -0
- package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.d.ts +18 -0
- package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.d.ts.map +1 -0
- package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.js +45 -0
- package/dist/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.js.map +1 -0
- package/dist/repository/media.repository.d.ts.map +1 -1
- package/dist/repository/media.repository.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +7 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +87 -3
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/permission-metadata-seeder.service.d.ts +1 -0
- package/dist/seeders/permission-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/permission-metadata-seeder.service.js +6 -1
- package/dist/seeders/permission-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +95 -13
- package/dist/services/authentication.service.d.ts +2 -2
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +52 -43
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +7 -1
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +85 -4
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/media-download-url.service.d.ts +22 -0
- package/dist/services/media-download-url.service.d.ts.map +1 -0
- package/dist/services/media-download-url.service.js +70 -0
- package/dist/services/media-download-url.service.js.map +1 -0
- package/dist/services/media-storage.utils.d.ts +24 -0
- package/dist/services/media-storage.utils.d.ts.map +1 -0
- package/dist/services/media-storage.utils.js +73 -0
- package/dist/services/media-storage.utils.js.map +1 -0
- package/dist/services/media.service.d.ts +20 -4
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/media.service.js +155 -55
- package/dist/services/media.service.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +4 -4
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +34 -63
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +5 -5
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.js +34 -38
- package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
- package/dist/services/role-metadata.service.d.ts +1 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +8 -5
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +25 -1
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.js +1 -1
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/seed.command.ts +12 -3
- package/src/constants/error-messages.ts +1 -0
- package/src/constants/media-file-types.ts +44 -0
- package/src/controllers/media.controller.ts +44 -24
- package/src/dtos/create-chatter-message.dto.ts +5 -0
- package/src/dtos/create-media.dto.ts +8 -3
- package/src/dtos/create-role-metadata.dto.ts +2 -0
- package/src/dtos/post-chatter-message.dto.ts +4 -0
- package/src/dtos/update-chatter-message.dto.ts +6 -0
- package/src/dtos/update-chatter-note-message.dto.ts +5 -0
- package/src/dtos/update-media.dto.ts +8 -3
- package/src/entities/chatter-message.entity.ts +3 -0
- package/src/entities/media.entity.ts +1 -1
- package/src/enums/auth-type.enum.ts +1 -0
- package/src/guards/authentication.guard.ts +3 -0
- package/src/guards/media-signed-url.guard.ts +41 -0
- package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +12 -24
- package/src/helpers/solid-core-error-codes-provider.service.ts +9 -0
- package/src/interfaces/chatter-mention-notification.interface.ts +29 -0
- package/src/interfaces.ts +20 -0
- package/src/jobs/chatter-mention-notification-email-queue-handler.service.ts +114 -0
- package/src/jobs/database/chatter-mention-notification-email-publisher-database.service.ts +23 -0
- package/src/jobs/database/chatter-mention-notification-email-queue-options-database.ts +9 -0
- package/src/jobs/database/chatter-mention-notification-email-subscriber-database.service.ts +32 -0
- package/src/jobs/rabbitmq/chatter-mention-notification-email-publisher.service.ts +23 -0
- package/src/jobs/rabbitmq/chatter-mention-notification-email-queue-options.ts +9 -0
- package/src/jobs/rabbitmq/chatter-mention-notification-email-subscriber.service.ts +30 -0
- package/src/jobs/redis/chatter-mention-notification-email-publisher-redis.service.ts +23 -0
- package/src/jobs/redis/chatter-mention-notification-email-queue-options-redis.ts +9 -0
- package/src/jobs/redis/chatter-mention-notification-email-subscriber-redis.service.ts +30 -0
- package/src/repository/media.repository.ts +1 -0
- package/src/seeders/module-metadata-seeder.service.ts +109 -3
- package/src/seeders/permission-metadata-seeder.service.ts +6 -1
- package/src/seeders/seed-data/email-templates/chatter-mention-notification.handlebars.html +200 -0
- package/src/seeders/seed-data/solid-core-metadata.json +95 -13
- package/src/services/authentication.service.ts +66 -55
- package/src/services/chatter-message.service.ts +97 -2
- package/src/services/media-download-url.service.ts +74 -0
- package/src/services/media-storage.utils.ts +70 -0
- package/src/services/media.service.ts +187 -59
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +49 -76
- package/src/services/mediaStorageProviders/file-storage-provider.ts +41 -48
- package/src/services/role-metadata.service.ts +17 -5
- package/src/solid-core.module.ts +35 -2
- package/src/subscribers/audit.subscriber.ts +1 -1
|
@@ -11,6 +11,7 @@ import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
|
|
|
11
11
|
import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
|
|
12
12
|
import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
|
|
13
13
|
import { lowerFirst } from 'src/helpers/string.helper';
|
|
14
|
+
import { ChatterMentionNotificationPayload } from 'src/interfaces/chatter-mention-notification.interface';
|
|
14
15
|
import { ChatterMessageDetailsRepository } from 'src/repository/chatter-message-details.repository';
|
|
15
16
|
import { ChatterMessageRepository } from 'src/repository/chatter-message.repository';
|
|
16
17
|
import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
|
|
@@ -24,6 +25,14 @@ import { getMediaStorageProvider } from './mediaStorageProviders';
|
|
|
24
25
|
import { RequestContextService } from './request-context.service';
|
|
25
26
|
import { Logger } from '@nestjs/common';
|
|
26
27
|
import { SolidIntrospectService } from './solid-introspect.service';
|
|
28
|
+
import { PublisherFactory } from './queues/publisher-factory.service';
|
|
29
|
+
|
|
30
|
+
interface ChatterMention {
|
|
31
|
+
username: string;
|
|
32
|
+
display_name?: string;
|
|
33
|
+
displayName?: string;
|
|
34
|
+
id?: string | number;
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
@Injectable()
|
|
29
38
|
export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
@@ -47,6 +56,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
47
56
|
private readonly modelMetadataRepo: ModelMetadataRepository,
|
|
48
57
|
readonly requestContextService: RequestContextService,
|
|
49
58
|
private readonly modelMetadataHelperService: ModelMetadataHelperService,
|
|
59
|
+
private readonly publisherFactory: PublisherFactory<ChatterMentionNotificationPayload>,
|
|
50
60
|
) {
|
|
51
61
|
super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef);
|
|
52
62
|
}
|
|
@@ -106,6 +116,78 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
106
116
|
.filter(v => Number.isInteger(v) && v > 0);
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
private parseMessageBodyMentions(value?: string): ChatterMention[] {
|
|
120
|
+
if (!value || typeof value !== 'string') return [];
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(value);
|
|
123
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private renderMentionTokens(messageBody: string, mentions: ChatterMention[]) {
|
|
130
|
+
return mentions.reduce((nextMessage, mention, index) => {
|
|
131
|
+
const username = mention.username;
|
|
132
|
+
if (!username) return nextMessage;
|
|
133
|
+
return nextMessage.replace(new RegExp(`\\{\\{\\s*${index}\\s*\\}\\}`, 'g'), `@${username}`);
|
|
134
|
+
}, messageBody || '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async publishChatterMentionNotifications(message: ChatterMessage, model: any) {
|
|
138
|
+
if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM || message.messageSubType !== CHATTER_MESSAGE_SUBTYPE.NOTE) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const mentions = this.parseMessageBodyMentions(message.messageBodyMentions);
|
|
143
|
+
const mentionUserIds = mentions
|
|
144
|
+
.map(mention => Number(mention.id))
|
|
145
|
+
.filter(id => Number.isInteger(id) && id > 0);
|
|
146
|
+
const uniqueMentionUserIds = Array.from(new Set(mentionUserIds));
|
|
147
|
+
if (uniqueMentionUserIds.length === 0) return;
|
|
148
|
+
|
|
149
|
+
const activeUser = this.requestContextService.getActiveUser();
|
|
150
|
+
|
|
151
|
+
const payload: ChatterMentionNotificationPayload = {
|
|
152
|
+
templateName: 'chatter-mention-notification',
|
|
153
|
+
mentions: mentions.map(mention => ({
|
|
154
|
+
id: Number(mention.id) || undefined,
|
|
155
|
+
username: mention.username,
|
|
156
|
+
displayName: mention.display_name || mention.displayName || mention.username,
|
|
157
|
+
})),
|
|
158
|
+
actor: {
|
|
159
|
+
id: activeUser?.sub,
|
|
160
|
+
username: activeUser?.username,
|
|
161
|
+
email: activeUser?.email,
|
|
162
|
+
},
|
|
163
|
+
noteBody: this.renderMentionTokens(message.messageBody, mentions),
|
|
164
|
+
entity: {
|
|
165
|
+
id: message.coModelEntityId,
|
|
166
|
+
modelName: model?.singularName || message.coModelName,
|
|
167
|
+
moduleName: model?.module?.name,
|
|
168
|
+
displayName: model?.displayName || message.coModelName,
|
|
169
|
+
userKey: message.modelUserKey,
|
|
170
|
+
},
|
|
171
|
+
parentEntity: 'chatterMessage',
|
|
172
|
+
parentEntityId: message.id,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await this.publisherFactory.publish(
|
|
177
|
+
{
|
|
178
|
+
payload,
|
|
179
|
+
parentEntity: 'chatterMessage',
|
|
180
|
+
parentEntityId: message.id,
|
|
181
|
+
retryCount: 3,
|
|
182
|
+
retryInterval: 5000,
|
|
183
|
+
},
|
|
184
|
+
'ChatterMentionNotificationEmailQueuePublisher',
|
|
185
|
+
);
|
|
186
|
+
} catch (error: any) {
|
|
187
|
+
this._logger.error(`Failed to publish chatter mention notification email job: ${error.message}`, error.stack);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
109
191
|
async markCompleted(id: number) {
|
|
110
192
|
const activeUser = this.requestContextService.getActiveUser();
|
|
111
193
|
if (!activeUser) {
|
|
@@ -146,10 +228,11 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
146
228
|
|
|
147
229
|
const removeAttachmentIds = this.parseAttachmentIds(updateDto?.removeAttachmentIds);
|
|
148
230
|
const hasMessageBody = typeof updateDto?.messageBody === 'string';
|
|
231
|
+
const hasMessageBodyMentions = typeof updateDto?.messageBodyMentions === 'string';
|
|
149
232
|
const trimmedMessageBody = (updateDto?.messageBody ?? '').trim();
|
|
150
233
|
const hasNewFiles = Array.isArray(files) && files.length > 0;
|
|
151
234
|
|
|
152
|
-
if (!hasMessageBody && removeAttachmentIds.length === 0 && !hasNewFiles) {
|
|
235
|
+
if (!hasMessageBody && !hasMessageBodyMentions && removeAttachmentIds.length === 0 && !hasNewFiles) {
|
|
153
236
|
throw new BadRequestException('No note changes submitted.');
|
|
154
237
|
}
|
|
155
238
|
|
|
@@ -160,6 +243,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
160
243
|
if (hasMessageBody) {
|
|
161
244
|
message.messageBody = trimmedMessageBody;
|
|
162
245
|
}
|
|
246
|
+
if (hasMessageBodyMentions) {
|
|
247
|
+
message.messageBodyMentions = updateDto.messageBodyMentions;
|
|
248
|
+
}
|
|
249
|
+
const model = await this.modelMetadataRepo.findOne({
|
|
250
|
+
where: { singularName: message.coModelName },
|
|
251
|
+
relations: { module: true }
|
|
252
|
+
});
|
|
163
253
|
message.updatedBy = activeUser.sub;
|
|
164
254
|
// Ensure updatedAt changes even for attachment-only edits.
|
|
165
255
|
message.updatedAt = new Date();
|
|
@@ -214,6 +304,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
214
304
|
}
|
|
215
305
|
}
|
|
216
306
|
|
|
307
|
+
await this.publishChatterMentionNotifications(savedMessage, model);
|
|
308
|
+
|
|
217
309
|
return savedMessage;
|
|
218
310
|
}
|
|
219
311
|
|
|
@@ -226,13 +318,14 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
226
318
|
chatterMessage.messageSubType = postDto.messageSubType || CHATTER_MESSAGE_SUBTYPE.CUSTOM;
|
|
227
319
|
chatterMessage.status = postDto.status ?? CHATTER_MESSAGE_STATUS.PENDING;
|
|
228
320
|
chatterMessage.messageBody = postDto.messageBody;
|
|
321
|
+
chatterMessage.messageBodyMentions = postDto.messageBodyMentions;
|
|
229
322
|
chatterMessage.coModelEntityId = postDto.coModelEntityId;
|
|
230
323
|
chatterMessage.coModelName = coModelName;
|
|
231
324
|
chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
|
|
232
325
|
|
|
233
326
|
const model = await this.modelMetadataRepo.findOne({
|
|
234
327
|
where: { singularName: coModelName },
|
|
235
|
-
relations: { userKeyField: true }
|
|
328
|
+
relations: { userKeyField: true, module: true }
|
|
236
329
|
});
|
|
237
330
|
chatterMessage.modelDisplayName = model?.displayName ?? null;
|
|
238
331
|
|
|
@@ -262,6 +355,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
262
355
|
}
|
|
263
356
|
}
|
|
264
357
|
|
|
358
|
+
await this.publishChatterMentionNotifications(savedMessage, model);
|
|
359
|
+
|
|
265
360
|
return savedMessage;
|
|
266
361
|
}
|
|
267
362
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { JwtService } from '@nestjs/jwt';
|
|
3
|
+
import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
|
|
4
|
+
import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
|
|
5
|
+
import { SettingService } from 'src/services/setting.service';
|
|
6
|
+
import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
|
|
7
|
+
import { getEffectiveS3Region } from './media-storage.utils';
|
|
8
|
+
import { ConfigService } from '@nestjs/config';
|
|
9
|
+
import { S3FileService } from './file';
|
|
10
|
+
|
|
11
|
+
export const MEDIA_DOWNLOAD_TOKEN_AUDIENCE = 'media-download';
|
|
12
|
+
const DEFAULT_SIGNED_URL_EXPIRY_MINUTES = 60;
|
|
13
|
+
|
|
14
|
+
export interface MediaDownloadTokenPayload {
|
|
15
|
+
mediaId: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class MediaDownloadUrlService {
|
|
20
|
+
private readonly logger = new Logger(MediaDownloadUrlService.name);
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly jwtService: JwtService,
|
|
24
|
+
private readonly s3FileService: S3FileService,
|
|
25
|
+
private readonly settingService: SettingService,
|
|
26
|
+
private readonly configService: ConfigService,
|
|
27
|
+
) { }
|
|
28
|
+
|
|
29
|
+
async resolveDownloadUrl(
|
|
30
|
+
mediaId: number,
|
|
31
|
+
relativeUri: string,
|
|
32
|
+
storageProvider?: MediaStorageProviderMetadata,
|
|
33
|
+
): Promise<string> {
|
|
34
|
+
const expiryMinutes = storageProvider?.signedUrlExpiry ?? DEFAULT_SIGNED_URL_EXPIRY_MINUTES;
|
|
35
|
+
|
|
36
|
+
if ((storageProvider?.type as MediaStorageProviderType) === MediaStorageProviderType.AwsS3) {
|
|
37
|
+
if (storageProvider?.bucketName) {
|
|
38
|
+
return this.s3FileService.getUrl(
|
|
39
|
+
`${storageProvider.bucketName}:${relativeUri}`,
|
|
40
|
+
{ region: getEffectiveS3Region(this.configService, storageProvider.region), expiresIn: expiryMinutes * 60 },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
this.logger.warn(`Media id ${mediaId}: AwsS3 provider missing bucketName; falling back to app-hosted token URL`);
|
|
44
|
+
}
|
|
45
|
+
// Filesystem (or misconfigured S3) has no native presigned-URL mechanism, so mint our
|
|
46
|
+
// own short-lived token and embed it in the download URL - the token alone authorizes
|
|
47
|
+
// the request (see MediaSignedUrlGuard), so it works in a plain <img src> with no
|
|
48
|
+
// Authorization header needed.
|
|
49
|
+
const token = await this.mintDownloadToken(mediaId, expiryMinutes);
|
|
50
|
+
const downloadPath = `/api/media/${mediaId}/download?token=${token}`;
|
|
51
|
+
const baseUrl = this.settingService.getConfigValue<SolidCoreSetting>('baseUrl');
|
|
52
|
+
return `${String(baseUrl).replace(/\/+$/, '')}${downloadPath}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getPrivateUrl(mediaId: number, relativeUri: string, storageProvider?: MediaStorageProviderMetadata): Promise<string> {
|
|
56
|
+
return this.resolveDownloadUrl(mediaId, relativeUri, storageProvider);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async verifyDownloadToken(token: string): Promise<MediaDownloadTokenPayload> {
|
|
60
|
+
return this.jwtService.verifyAsync<MediaDownloadTokenPayload>(token, {
|
|
61
|
+
secret: this.settingService.getConfigValue<SolidCoreSetting>('secret'),
|
|
62
|
+
audience: MEDIA_DOWNLOAD_TOKEN_AUDIENCE,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private mintDownloadToken(mediaId: number, expiryMinutes: number): Promise<string> {
|
|
67
|
+
const payload: MediaDownloadTokenPayload = { mediaId };
|
|
68
|
+
return this.jwtService.signAsync(payload, {
|
|
69
|
+
secret: this.settingService.getConfigValue<SolidCoreSetting>('secret'),
|
|
70
|
+
audience: MEDIA_DOWNLOAD_TOKEN_AUDIENCE,
|
|
71
|
+
expiresIn: expiryMinutes * 60,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ConfigService } from '@nestjs/config';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { FieldMetadata } from 'src/entities/field-metadata.entity';
|
|
4
|
+
import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
|
|
5
|
+
import { SettingService } from 'src/services/setting.service';
|
|
6
|
+
import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from 'src/services/settings/default-settings-provider.service';
|
|
7
|
+
import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export interface MediaRecordCreateInput {
|
|
11
|
+
entityId: number;
|
|
12
|
+
modelMetadataId: number;
|
|
13
|
+
mediaStorageProviderMetadataId: number;
|
|
14
|
+
fieldMetadataId: number;
|
|
15
|
+
relativeUri: string;
|
|
16
|
+
mimeType?: string;
|
|
17
|
+
fileSize?: number;
|
|
18
|
+
originalFileName?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Builds the stored file name format shared by media storage implementations.
|
|
22
|
+
export function buildStoredMediaFileName(file: Pick<Express.Multer.File, 'filename' | 'originalname'>): string {
|
|
23
|
+
return `${file.filename}-${file.originalname}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Resolves the on-disk path for a media file based on settings and provider visibility.
|
|
27
|
+
export function buildDiskMediaPath(fileName: string,settingService: Pick<SettingService, 'getConfigValue'>,storageProvider?: Pick<MediaStorageProviderMetadata, 'localPath' | 'isPublic'>,): string {
|
|
28
|
+
const publicBase = settingService.getConfigValue<SolidCoreSetting>('fileStorageDir') || DEFAULT_MEDIA_FILE_STORAGE_DIR;
|
|
29
|
+
const privateBase = `${publicBase}-private`;
|
|
30
|
+
// const localPath = storageProvider?.localPath;
|
|
31
|
+
// const isUsingLegacyDefaultPath = localPath === publicBase || localPath === privateBase || localPath === privateBase;
|
|
32
|
+
const providerBase = storageProvider?.isPublic === false ? privateBase : publicBase
|
|
33
|
+
|
|
34
|
+
if ( path.isAbsolute(fileName) || fileName.startsWith(`${publicBase}/`) || fileName.startsWith(`${privateBase}/`) ) {
|
|
35
|
+
return fileName;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `${providerBase}/${fileName}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Returns the S3 region from the provider when present, otherwise falls back to app config.
|
|
42
|
+
export function getEffectiveS3Region(configService: Pick<ConfigService, 'get'>, providerRegion?: string): string | undefined {
|
|
43
|
+
return providerRegion || configService.get<string>('S3_AWS_REGION_NAME');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolves the effective public/private flag for a media storage provider.
|
|
47
|
+
export function resolveMediaIsPublic(storageProvider?: Pick<MediaStorageProviderMetadata, 'isPublic'>): boolean | undefined {
|
|
48
|
+
if (!storageProvider) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return storageProvider.isPublic !== false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Builds the shared media record payload used before saving media metadata to the database.
|
|
56
|
+
export function buildMediaRecordCreateInput(
|
|
57
|
+
entityId: number,
|
|
58
|
+
mediaFieldMetadata: Pick<FieldMetadata, 'id' | 'model'> & { model: { id: number } },
|
|
59
|
+
storageProvider: Pick<MediaStorageProviderMetadata, 'id'>,
|
|
60
|
+
overrides: Pick<MediaRecordCreateInput, 'relativeUri'>
|
|
61
|
+
& Partial<Omit<MediaRecordCreateInput, 'entityId' | 'modelMetadataId' | 'mediaStorageProviderMetadataId' | 'fieldMetadataId' | 'relativeUri'>>,
|
|
62
|
+
): MediaRecordCreateInput {
|
|
63
|
+
return {
|
|
64
|
+
entityId,
|
|
65
|
+
modelMetadataId: mediaFieldMetadata.model.id,
|
|
66
|
+
mediaStorageProviderMetadataId: storageProvider.id,
|
|
67
|
+
fieldMetadataId: mediaFieldMetadata.id,
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import { forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
1
|
+
import { forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from "@nestjs/core";
|
|
3
3
|
import { InjectEntityManager } from '@nestjs/typeorm';
|
|
4
4
|
import { EntityManager, In } from 'typeorm';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
-
import {
|
|
7
|
-
import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service";
|
|
6
|
+
import { Readable } from 'stream';
|
|
8
7
|
|
|
9
8
|
import { ConfigService } from '@nestjs/config';
|
|
10
9
|
import { CRUDService } from 'src/services/crud.service';
|
|
11
10
|
import { DiskFileService, S3FileService } from 'src/services/file';
|
|
11
|
+
import { MediaDownloadUrlService } from 'src/services/media-download-url.service';
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
15
15
|
import { BasicFilterDto } from 'src/dtos/basic-filters.dto';
|
|
16
16
|
import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
|
|
17
17
|
import { Media } from 'src/entities/media.entity';
|
|
18
|
+
import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
|
|
18
19
|
import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
|
|
19
20
|
import { MediaStorageProviderMetadataRepository } from 'src/repository/media-storage-provider-metadata.repository';
|
|
20
21
|
import { MediaRepository } from 'src/repository/media.repository';
|
|
21
22
|
import { ModelMetadataRepository } from 'src/repository/model-metadata.repository';
|
|
23
|
+
import { buildDiskMediaPath, buildStoredMediaFileName, getEffectiveS3Region, resolveMediaIsPublic, } from 'src/services/media-storage.utils';
|
|
22
24
|
import { getMediaStorageProvider } from "./mediaStorageProviders";
|
|
23
25
|
|
|
24
|
-
|
|
25
26
|
@Injectable()
|
|
26
27
|
export class MediaService extends CRUDService<Media> {
|
|
28
|
+
private readonly logger = new Logger(MediaService.name);
|
|
29
|
+
|
|
27
30
|
constructor(
|
|
28
31
|
readonly configService: ConfigService,
|
|
29
32
|
readonly diskFileService: DiskFileService,
|
|
30
33
|
readonly s3FileService: S3FileService,
|
|
34
|
+
private readonly mediaDownloadUrlService: MediaDownloadUrlService,
|
|
31
35
|
@InjectEntityManager()
|
|
32
36
|
readonly entityManager: EntityManager,
|
|
33
37
|
// @InjectRepository(Media, 'default')
|
|
@@ -51,75 +55,73 @@ export class MediaService extends CRUDService<Media> {
|
|
|
51
55
|
async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}) {
|
|
52
56
|
const data = await super.find(basicFilterDto, solidRequestContext);
|
|
53
57
|
if (data.records) {
|
|
54
|
-
|
|
55
|
-
for (const media of data.records) {
|
|
56
|
-
const mediaStorageProvider = media.mediaStorageProviderMetadata;
|
|
57
|
-
|
|
58
|
-
if (mediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
|
|
59
|
-
media.relativeUri = await this.diskFileService.getUrl(this.getFullFilePathForDisk(media.relativeUri));
|
|
60
|
-
} else if (mediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
|
|
61
|
-
media.relativeUri = await this.s3FileService.getUrl(`${mediaStorageProvider.bucketName}:${media.relativeUri}`, { region: mediaStorageProvider.region });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
58
|
+
await this.decorateMediaRecords(data.records);
|
|
64
59
|
}
|
|
65
60
|
if (data.groupRecords) {
|
|
66
|
-
|
|
67
61
|
for (const group of data.groupRecords) {
|
|
68
|
-
|
|
69
|
-
const mediaStorageProvider = media.mediaStorageProviderMetadata;
|
|
70
|
-
|
|
71
|
-
if (mediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
|
|
72
|
-
media.relativeUri = await this.diskFileService.getUrl(this.getFullFilePathForDisk(media.relativeUri));
|
|
73
|
-
}
|
|
74
|
-
else if (mediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
|
|
75
|
-
media.relativeUri = await this.s3FileService.getUrl(`${mediaStorageProvider.bucketName}:${media.relativeUri}`, { region: mediaStorageProvider.region });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
62
|
+
await this.decorateMediaRecords(group.groupData.records);
|
|
78
63
|
}
|
|
79
64
|
}
|
|
80
65
|
return data
|
|
81
66
|
}
|
|
82
67
|
|
|
83
|
-
async
|
|
68
|
+
async findOne(id: number, query: any = {}, solidRequestContext: any = {}) {
|
|
69
|
+
const media = await super.findOne(id, query, solidRequestContext);
|
|
70
|
+
if (media) {
|
|
71
|
+
await this.decorateMediaRecord(media);
|
|
72
|
+
}
|
|
73
|
+
return media;
|
|
74
|
+
}
|
|
84
75
|
|
|
85
|
-
|
|
76
|
+
async upload(createDto: any, files: Array<Express.Multer.File>, _solidRequestContext: any = {}) {
|
|
77
|
+
|
|
78
|
+
if (!files || files.length === 0) {
|
|
86
79
|
throw new NotFoundException(ERROR_MESSAGES.FILE_NOT_FOUND);
|
|
87
80
|
|
|
88
81
|
}
|
|
89
|
-
const savedMedias = [];
|
|
90
|
-
for (let i = 0; i < files.length; i++) {
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
83
|
+
createDto['fieldMetadata'] = await this.fieldMetadataRepo.findOne({
|
|
84
|
+
where: {
|
|
85
|
+
id: createDto['fieldMetadataId']
|
|
86
|
+
},
|
|
87
|
+
relations: ['mediaStorageProvider', 'model'],
|
|
88
|
+
});
|
|
89
|
+
createDto['modelMetadata'] = await this.modelMetadataRepo.findOne({
|
|
90
|
+
where: {
|
|
91
|
+
id: createDto['modelMetadataId']
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
createDto['mediaStorageProviderMetadata'] = await this.mediaStorageProviderMetadataRepo.findOne({
|
|
95
|
+
where: {
|
|
96
|
+
id: createDto['mediaStorageProviderMetadataId']
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
createDto['mediaStorageProviderMetadata'] = createDto['mediaStorageProviderMetadata'] || createDto['fieldMetadata']?.mediaStorageProvider;
|
|
100
|
+
createDto['modelMetadata'] = createDto['modelMetadata'] || createDto['fieldMetadata']?.model;
|
|
107
101
|
|
|
102
|
+
if (!createDto['mediaStorageProviderMetadata']) {
|
|
103
|
+
throw new NotFoundException('Media storage provider metadata not found');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const savedMedias = [];
|
|
107
|
+
for (let i = 0; i < files.length; i++) {
|
|
108
108
|
const file = files[i];
|
|
109
|
+
const storageProvider = createDto.mediaStorageProviderMetadata as MediaStorageProviderMetadata;
|
|
110
|
+
const fileName = buildStoredMediaFileName(file);
|
|
109
111
|
|
|
110
|
-
switch (
|
|
112
|
+
switch (storageProvider.type) {
|
|
111
113
|
case MediaStorageProviderType.Filesystem:
|
|
112
|
-
const fileStoragePath =
|
|
114
|
+
const fileStoragePath = buildDiskMediaPath(fileName, this.settingService, storageProvider);
|
|
113
115
|
await this.diskFileService.copy(file.path, fileStoragePath);
|
|
114
|
-
createDto['relativeUri'] =
|
|
116
|
+
createDto['relativeUri'] = fileName;
|
|
115
117
|
break;
|
|
116
118
|
case MediaStorageProviderType.AwsS3:
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
+
const bucketName = storageProvider.bucketName;
|
|
120
|
+
const region = getEffectiveS3Region(this.configService, storageProvider.region);
|
|
119
121
|
|
|
120
122
|
// Read file from disk and upload to S3
|
|
121
123
|
const fileData = await this.diskFileService.read(file.path);
|
|
122
|
-
await this.s3FileService.write(`${bucketName}:${fileName}`, fileData, { contentType: file.mimetype });
|
|
124
|
+
await this.s3FileService.write(`${bucketName}:${fileName}`, fileData, { contentType: file.mimetype, region });
|
|
123
125
|
|
|
124
126
|
createDto['relativeUri'] = fileName;
|
|
125
127
|
break;
|
|
@@ -129,7 +131,8 @@ export class MediaService extends CRUDService<Media> {
|
|
|
129
131
|
// Delete temp file from disk
|
|
130
132
|
await this.diskFileService.delete(file.path);
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
delete createDto['isPublic'];
|
|
135
|
+
const media = this.repo.create(createDto as Partial<Media>) as Media;
|
|
133
136
|
const savedMedia = await this.repo.save(media);
|
|
134
137
|
savedMedias.push(savedMedia)
|
|
135
138
|
}
|
|
@@ -157,17 +160,142 @@ export class MediaService extends CRUDService<Media> {
|
|
|
157
160
|
return this.repo.remove(media);
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
async fileDownloadStream(media: Media): Promise<{ stream: Readable | null, fileName: string, mimeType: string, redirectUrl?: string }> {
|
|
164
|
+
const loadedMedia = await this.repo.findOne({
|
|
165
|
+
where: { id: media.id },
|
|
166
|
+
relations: ['mediaStorageProviderMetadata'],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!loadedMedia || !loadedMedia.mediaStorageProviderMetadata) {
|
|
170
|
+
throw new NotFoundException(`Media with id ${media.id} not found`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fileName = loadedMedia.originalFileName || path.basename(loadedMedia.relativeUri || `${loadedMedia.id}`);
|
|
174
|
+
const mimeType = loadedMedia.mimeType || 'application/octet-stream';
|
|
175
|
+
|
|
176
|
+
switch (loadedMedia.mediaStorageProviderMetadata.type as MediaStorageProviderType) {
|
|
177
|
+
case MediaStorageProviderType.Filesystem:
|
|
178
|
+
return {
|
|
179
|
+
stream: await this.diskFileService.readStream(
|
|
180
|
+
buildDiskMediaPath(loadedMedia.relativeUri, this.settingService, loadedMedia.mediaStorageProviderMetadata),
|
|
181
|
+
),
|
|
182
|
+
fileName,
|
|
183
|
+
mimeType,
|
|
184
|
+
};
|
|
185
|
+
default:
|
|
186
|
+
throw new Error(`Unsupported media storage provider type ${loadedMedia.mediaStorageProviderMetadata.type}`);
|
|
165
187
|
}
|
|
166
|
-
return `${base}/${fileName}`;
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
async delete(id: number, solidRequestContext: any = {}) {
|
|
191
|
+
const media = await this.repo.findOne({
|
|
192
|
+
where: { id },
|
|
193
|
+
relations: ['mediaStorageProviderMetadata'],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await super.delete(id, solidRequestContext);
|
|
197
|
+
|
|
198
|
+
if (media) {
|
|
199
|
+
await this.deletePhysicalFile(media);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
async deleteMany(ids: number[], solidRequestContext: any = {}) {
|
|
206
|
+
const mediaRecords = await this.repo.find({
|
|
207
|
+
where: {
|
|
208
|
+
id: In(ids),
|
|
209
|
+
},
|
|
210
|
+
relations: ['mediaStorageProviderMetadata'],
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await super.deleteMany(ids, solidRequestContext);
|
|
214
|
+
|
|
215
|
+
for (const media of mediaRecords) {
|
|
216
|
+
await this.deletePhysicalFile(media);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async decorateMediaRecords(medias: Media[]): Promise<void> {
|
|
223
|
+
for (const media of medias) {
|
|
224
|
+
await this.decorateMediaRecord(media);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async decorateMediaRecord(media: Media): Promise<void> {
|
|
229
|
+
const mediaStorageProvider = await this.resolveMediaStorageProvider(media);
|
|
230
|
+
media.relativeUri = await this.resolveMediaUrl(media, mediaStorageProvider);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async resolveMediaUrl(media: Media, mediaStorageProvider?: MediaStorageProviderMetadata): Promise<string> {
|
|
234
|
+
const resolvedMediaStorageProvider = mediaStorageProvider || await this.resolveMediaStorageProvider(media);
|
|
235
|
+
const isPublic = resolveMediaIsPublic(resolvedMediaStorageProvider);
|
|
236
|
+
|
|
237
|
+
if (isPublic === false) {
|
|
238
|
+
return this.mediaDownloadUrlService.getPrivateUrl(media.id, media.relativeUri, resolvedMediaStorageProvider);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (resolvedMediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
|
|
242
|
+
return this.diskFileService.getUrl(buildDiskMediaPath(media.relativeUri, this.settingService, resolvedMediaStorageProvider));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (resolvedMediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
|
|
246
|
+
return this.s3FileService.getUrl(
|
|
247
|
+
`${resolvedMediaStorageProvider.bucketName}:${media.relativeUri}`,
|
|
248
|
+
{ region: getEffectiveS3Region(this.configService, resolvedMediaStorageProvider.region), expiresIn: 0 },
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return media.relativeUri;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async deletePhysicalFile(media: Media): Promise<void> {
|
|
256
|
+
if (!media?.relativeUri) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const mediaStorageProvider = await this.resolveMediaStorageProvider(media);
|
|
262
|
+
if (!mediaStorageProvider) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
switch (mediaStorageProvider.type as MediaStorageProviderType) {
|
|
267
|
+
case MediaStorageProviderType.Filesystem:
|
|
268
|
+
await this.diskFileService.delete(buildDiskMediaPath(media.relativeUri, this.settingService, mediaStorageProvider));
|
|
269
|
+
return;
|
|
270
|
+
case MediaStorageProviderType.AwsS3:
|
|
271
|
+
await this.s3FileService.delete(
|
|
272
|
+
`${mediaStorageProvider.bucketName}:${media.relativeUri}`,
|
|
273
|
+
{ region: getEffectiveS3Region(this.configService, mediaStorageProvider.region) },
|
|
274
|
+
);
|
|
275
|
+
return;
|
|
276
|
+
default:
|
|
277
|
+
this.logger.warn(`Skipping physical delete for unsupported media storage provider type ${mediaStorageProvider.type}`);
|
|
278
|
+
}
|
|
279
|
+
} catch (error: any) {
|
|
280
|
+
const message = error?.message ?? String(error);
|
|
281
|
+
this.logger.warn(`Failed to delete physical media file for media id ${media.id}: ${message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async resolveMediaStorageProvider(media: Media): Promise<MediaStorageProviderMetadata | undefined> {
|
|
286
|
+
if (media.mediaStorageProviderMetadata) {
|
|
287
|
+
return media.mediaStorageProviderMetadata;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!media.id) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const loadedMedia = await this.repo.findOne({
|
|
295
|
+
where: { id: media.id },
|
|
296
|
+
relations: ['mediaStorageProviderMetadata'],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return loadedMedia?.mediaStorageProviderMetadata;
|
|
300
|
+
}
|
|
173
301
|
}
|