Add adaptive media variants for weak networks
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
@@ -12,6 +12,10 @@ import {
|
||||
normalizeWaveformPeaks,
|
||||
} from '../../common/utils/waveform.util';
|
||||
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
|
||||
import {
|
||||
ImageProcessingService,
|
||||
UploadedImageFile,
|
||||
} from '../../infrastructure/storage/image-processing.service';
|
||||
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
|
||||
import {
|
||||
UploadedVideoFile,
|
||||
@@ -26,7 +30,7 @@ import { CreatePostDto } from './dto/create-post.dto';
|
||||
import { PostQueryDto } from './dto/post-query.dto';
|
||||
import { ReelQueryDto } from './dto/reel-query.dto';
|
||||
import { UpdatePostDto } from './dto/update-post.dto';
|
||||
import { PostDocument } from './schemas/post.schema';
|
||||
import { PostDocument, PostMediaVariantSet } from './schemas/post.schema';
|
||||
import { PostsRepository } from './posts.repository';
|
||||
|
||||
type PostMediaMetadataInput = Pick<
|
||||
@@ -52,6 +56,12 @@ type SavedVideoUpload = {
|
||||
videoUrl: string;
|
||||
hlsUrl: string;
|
||||
thumbnailUrl: string;
|
||||
thumbnailVariants: PostMediaVariantSet | null;
|
||||
};
|
||||
|
||||
type SavedImageUpload = {
|
||||
primaryUrl: string;
|
||||
variants: PostMediaVariantSet;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -62,6 +72,7 @@ export class PostsService {
|
||||
private readonly postsRepository: PostsRepository,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly storageService: ManagedStorageService,
|
||||
private readonly imageProcessingService: ImageProcessingService,
|
||||
private readonly videoProcessingService: VideoProcessingService,
|
||||
private readonly feedVersionService: FeedVersionService,
|
||||
private readonly notificationsService: NotificationsService,
|
||||
@@ -98,13 +109,17 @@ export class PostsService {
|
||||
throw new BadRequestException('Post can contain either images or audio, not both');
|
||||
}
|
||||
|
||||
const uploadedImageUrls = await this.saveImageFiles(imageFiles);
|
||||
const savedImageUploads = await this.saveImageFiles(imageFiles);
|
||||
const uploadedImageUrls = savedImageUploads.map((item) => item.primaryUrl);
|
||||
const uploadedImageVariants = savedImageUploads.map((item) => item.variants);
|
||||
const savedVideoUpload = videoFile ? await this.saveVideoUpload(videoFile) : null;
|
||||
const uploadedVideoUrl = savedVideoUpload?.videoUrl ?? '';
|
||||
const uploadedHlsUrl = savedVideoUpload?.hlsUrl ?? '';
|
||||
const uploadedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
|
||||
const uploadedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
|
||||
const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
|
||||
const finalImageUrls = uploadedImageUrls.length ? uploadedImageUrls : inputImageUrls;
|
||||
const finalImageVariants = uploadedImageVariants.length ? uploadedImageVariants : [];
|
||||
const finalVideoUrl = uploadedVideoUrl || dto.videoUrl || '';
|
||||
const finalAudioUrl = uploadedAudioUrl || dto.audioUrl || '';
|
||||
const finalContent = dto.content?.trim() ?? '';
|
||||
@@ -128,9 +143,11 @@ export class PostsService {
|
||||
post = await this.postsRepository.create(userId, {
|
||||
content: finalContent,
|
||||
imageUrls: finalImageUrls,
|
||||
imageVariants: finalImageVariants,
|
||||
videoUrl: finalVideoUrl,
|
||||
hlsUrl: uploadedHlsUrl,
|
||||
audioUrl: finalAudioUrl,
|
||||
thumbnailVariants: uploadedThumbnailVariants,
|
||||
taggedUserIds,
|
||||
mentionUsernames: mentionResolution.mentionUsernames,
|
||||
location,
|
||||
@@ -143,10 +160,12 @@ export class PostsService {
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.all([
|
||||
...uploadedImageUrls.map((url) => this.deleteManagedPostMedia(url)),
|
||||
...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
|
||||
uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
|
||||
uploadedHlsUrl ? this.storageService.deleteContainingDirectory(uploadedHlsUrl) : Promise.resolve(),
|
||||
uploadedThumbnailUrl ? this.deleteManagedPostMedia(uploadedThumbnailUrl) : Promise.resolve(),
|
||||
uploadedThumbnailUrl
|
||||
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
|
||||
: Promise.resolve(),
|
||||
uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
|
||||
]);
|
||||
throw error;
|
||||
@@ -199,13 +218,21 @@ export class PostsService {
|
||||
throw new BadRequestException('Post can contain either images or audio, not both');
|
||||
}
|
||||
|
||||
const uploadedImageUrls = await this.saveImageFiles(imageFiles);
|
||||
const savedImageUploads = await this.saveImageFiles(imageFiles);
|
||||
const uploadedImageUrls = savedImageUploads.map((item) => item.primaryUrl);
|
||||
const uploadedImageVariants = savedImageUploads.map((item) => item.variants);
|
||||
const savedVideoUpload = videoFile ? await this.saveVideoUpload(videoFile) : null;
|
||||
const uploadedVideoUrl = savedVideoUpload?.videoUrl ?? '';
|
||||
const uploadedHlsUrl = savedVideoUpload?.hlsUrl ?? '';
|
||||
const uploadedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
|
||||
const uploadedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
|
||||
const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
|
||||
const hasImageUpdate = typeof dto.imageUrls !== 'undefined' || imageFiles.length > 0;
|
||||
const existingImageVariants = Array.isArray((post as any).imageVariants)
|
||||
? (((post as any).imageVariants ?? []) as PostMediaVariantSet[])
|
||||
: [];
|
||||
const existingThumbnailVariants =
|
||||
((post as any).thumbnailVariants as PostMediaVariantSet | null | undefined) ?? null;
|
||||
|
||||
const hasVideoUpdate = typeof dto.videoUrl !== 'undefined' || !!videoFile;
|
||||
const hasAudioUpdate = typeof dto.audioUrl !== 'undefined' || !!audioFile;
|
||||
@@ -214,6 +241,11 @@ export class PostsService {
|
||||
? uploadedImageUrls
|
||||
: inputImageUrls
|
||||
: post.imageUrls ?? [];
|
||||
const nextImageVariants = hasImageUpdate
|
||||
? imageFiles.length
|
||||
? uploadedImageVariants
|
||||
: []
|
||||
: existingImageVariants;
|
||||
|
||||
const nextVideoUrl = hasVideoUpdate
|
||||
? videoFile
|
||||
@@ -226,6 +258,11 @@ export class PostsService {
|
||||
? uploadedAudioUrl
|
||||
: dto.audioUrl ?? ''
|
||||
: post.audioUrl ?? '';
|
||||
const nextThumbnailVariants = videoFile
|
||||
? uploadedThumbnailVariants
|
||||
: typeof dto.thumbnailUrl === 'string'
|
||||
? null
|
||||
: existingThumbnailVariants;
|
||||
const nextPostType = this.resolvePostType(nextImageUrls, nextVideoUrl, nextAudioUrl);
|
||||
const nextContent = typeof dto.content === 'string' ? dto.content.trim() : post.content ?? '';
|
||||
const nextTaggedUserIds =
|
||||
@@ -272,7 +309,9 @@ export class PostsService {
|
||||
...dto,
|
||||
content: nextContent,
|
||||
imageUrls: nextImageUrls,
|
||||
imageVariants: nextImageVariants,
|
||||
hlsUrl: nextHlsUrl,
|
||||
thumbnailVariants: nextThumbnailVariants,
|
||||
taggedUserIds: nextTaggedUserIds,
|
||||
mentionUsernames: mentionResolution.mentionUsernames,
|
||||
location: nextLocation,
|
||||
@@ -300,40 +339,51 @@ export class PostsService {
|
||||
payload.videoUrl = '';
|
||||
payload.audioUrl = '';
|
||||
payload.hlsUrl = '';
|
||||
payload.thumbnailVariants = null;
|
||||
}
|
||||
|
||||
if (videoFile) {
|
||||
payload.videoUrl = uploadedVideoUrl;
|
||||
payload.hlsUrl = uploadedHlsUrl;
|
||||
payload.thumbnailVariants = uploadedThumbnailVariants;
|
||||
payload.imageUrls = [];
|
||||
payload.imageVariants = [];
|
||||
payload.audioUrl = '';
|
||||
}
|
||||
if (audioFile) {
|
||||
payload.audioUrl = uploadedAudioUrl;
|
||||
payload.imageUrls = [];
|
||||
payload.imageVariants = [];
|
||||
payload.videoUrl = '';
|
||||
payload.hlsUrl = '';
|
||||
}
|
||||
if (nextPostType !== PostType.AUDIO && nextPostType !== PostType.VIDEO) {
|
||||
payload.thumbnailVariants = null;
|
||||
}
|
||||
|
||||
let updated: PostDocument | null;
|
||||
try {
|
||||
updated = await this.postsRepository.updateById(postId, payload as any);
|
||||
} catch (error) {
|
||||
await Promise.all([
|
||||
...uploadedImageUrls.map((url) => this.deleteManagedPostMedia(url)),
|
||||
...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
|
||||
uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
|
||||
uploadedHlsUrl ? this.storageService.deleteContainingDirectory(uploadedHlsUrl) : Promise.resolve(),
|
||||
uploadedThumbnailUrl ? this.deleteManagedPostMedia(uploadedThumbnailUrl) : Promise.resolve(),
|
||||
uploadedThumbnailUrl
|
||||
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
|
||||
: Promise.resolve(),
|
||||
uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
|
||||
]);
|
||||
throw error;
|
||||
}
|
||||
if (!updated) {
|
||||
await Promise.all([
|
||||
...uploadedImageUrls.map((url) => this.deleteManagedPostMedia(url)),
|
||||
...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
|
||||
uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
|
||||
uploadedHlsUrl ? this.storageService.deleteContainingDirectory(uploadedHlsUrl) : Promise.resolve(),
|
||||
uploadedThumbnailUrl ? this.deleteManagedPostMedia(uploadedThumbnailUrl) : Promise.resolve(),
|
||||
uploadedThumbnailUrl
|
||||
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
|
||||
: Promise.resolve(),
|
||||
uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
|
||||
]);
|
||||
throw new NotFoundException('Post not found');
|
||||
@@ -346,17 +396,18 @@ export class PostsService {
|
||||
await this.storageService.deleteContainingDirectory(post.hlsUrl ?? '');
|
||||
}
|
||||
if ((post.thumbnailUrl ?? '') !== (updated.thumbnailUrl ?? '')) {
|
||||
await this.deleteManagedPostMedia(post.thumbnailUrl ?? '');
|
||||
await this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants);
|
||||
}
|
||||
if (hasAudioUpdate && (post.audioUrl ?? '') !== (updated.audioUrl ?? '')) {
|
||||
await this.deleteManagedPostMedia(post.audioUrl ?? '');
|
||||
}
|
||||
if (hasImageUpdate) {
|
||||
const nextImageSet = new Set(updated.imageUrls ?? []);
|
||||
const existingImageAssets = this.buildSavedImageAssets(post.imageUrls ?? [], existingImageVariants);
|
||||
await Promise.all(
|
||||
(post.imageUrls ?? [])
|
||||
.filter((url) => !nextImageSet.has(url))
|
||||
.map((url) => this.deleteManagedPostMedia(url)),
|
||||
existingImageAssets
|
||||
.filter((asset) => !nextImageSet.has(asset.primaryUrl))
|
||||
.map((asset) => this.deleteSavedImageAsset(asset)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,11 +433,19 @@ export class PostsService {
|
||||
}
|
||||
|
||||
await this.postsRepository.deleteById(postId, userId);
|
||||
const existingImageVariants = Array.isArray((post as any).imageVariants)
|
||||
? (((post as any).imageVariants ?? []) as PostMediaVariantSet[])
|
||||
: [];
|
||||
const existingThumbnailVariants =
|
||||
((post as any).thumbnailVariants as PostMediaVariantSet | null | undefined) ?? null;
|
||||
await Promise.all([
|
||||
...(post.imageUrls ?? []).map((url) => this.deleteManagedPostMedia(url)),
|
||||
...this.buildSavedImageAssets(post.imageUrls ?? [], existingImageVariants).map((asset) =>
|
||||
this.deleteSavedImageAsset(asset),
|
||||
),
|
||||
this.deleteManagedPostMedia(post.videoUrl ?? ''),
|
||||
this.storageService.deleteContainingDirectory(post.hlsUrl ?? ''),
|
||||
this.deleteManagedPostMedia(post.audioUrl ?? ''),
|
||||
this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants),
|
||||
]);
|
||||
await this.usersRepository.incrementPostsCount(userId, -1);
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
@@ -623,12 +682,20 @@ export class PostsService {
|
||||
throw new NotFoundException('Post not found');
|
||||
}
|
||||
|
||||
const existingImageVariants = Array.isArray((post as any).imageVariants)
|
||||
? (((post as any).imageVariants ?? []) as PostMediaVariantSet[])
|
||||
: [];
|
||||
const existingThumbnailVariants =
|
||||
((post as any).thumbnailVariants as PostMediaVariantSet | null | undefined) ?? null;
|
||||
await this.postsRepository.deleteById(postId, superAdminIdentifier);
|
||||
await Promise.all([
|
||||
...(post.imageUrls ?? []).map((url) => this.deleteManagedPostMedia(url)),
|
||||
...this.buildSavedImageAssets(post.imageUrls ?? [], existingImageVariants).map((asset) =>
|
||||
this.deleteSavedImageAsset(asset),
|
||||
),
|
||||
this.deleteManagedPostMedia(post.videoUrl ?? ''),
|
||||
this.storageService.deleteContainingDirectory(post.hlsUrl ?? ''),
|
||||
this.deleteManagedPostMedia(post.audioUrl ?? ''),
|
||||
this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants),
|
||||
]);
|
||||
const authorId = this.extractEntityId(post.authorId);
|
||||
if (authorId) {
|
||||
@@ -938,6 +1005,7 @@ export class PostsService {
|
||||
let videoUrl = '';
|
||||
let hlsUrl = '';
|
||||
let thumbnailUrl = '';
|
||||
let thumbnailVariants: PostMediaVariantSet | null = null;
|
||||
const hlsFolderName = `stream-${new Types.ObjectId().toString()}`;
|
||||
|
||||
try {
|
||||
@@ -958,21 +1026,22 @@ export class PostsService {
|
||||
}
|
||||
|
||||
if (optimized.generatedThumbnail) {
|
||||
thumbnailUrl = await this.storageService.saveFile({
|
||||
folderSegments: ['posts', 'thumbnails'],
|
||||
extension: optimized.generatedThumbnail.extension,
|
||||
const savedThumbnail = await this.saveResponsiveImageAsset('thumbnails', {
|
||||
buffer: optimized.generatedThumbnail.buffer,
|
||||
contentType: optimized.generatedThumbnail.contentType,
|
||||
fileNamePrefix: 'thumbnail',
|
||||
size: optimized.generatedThumbnail.buffer.length,
|
||||
mimetype: optimized.generatedThumbnail.contentType,
|
||||
originalname: `thumbnail${optimized.generatedThumbnail.extension}`,
|
||||
});
|
||||
thumbnailUrl = savedThumbnail.primaryUrl;
|
||||
thumbnailVariants = savedThumbnail.variants;
|
||||
}
|
||||
|
||||
return { videoUrl, hlsUrl, thumbnailUrl };
|
||||
return { videoUrl, hlsUrl, thumbnailUrl, thumbnailVariants };
|
||||
} catch (error) {
|
||||
await Promise.all([
|
||||
videoUrl ? this.deleteManagedPostMedia(videoUrl) : Promise.resolve(),
|
||||
hlsUrl ? this.storageService.deleteContainingDirectory(hlsUrl) : Promise.resolve(),
|
||||
thumbnailUrl ? this.deleteManagedPostMedia(thumbnailUrl) : Promise.resolve(),
|
||||
thumbnailUrl ? this.deleteThumbnailAsset(thumbnailUrl, thumbnailVariants) : Promise.resolve(),
|
||||
]);
|
||||
throw error;
|
||||
}
|
||||
@@ -1097,7 +1166,7 @@ export class PostsService {
|
||||
|
||||
private async saveImageFiles(
|
||||
files: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>,
|
||||
): Promise<string[]> {
|
||||
): Promise<SavedImageUpload[]> {
|
||||
if (!files.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -1105,18 +1174,150 @@ export class PostsService {
|
||||
throw new BadRequestException('Post can contain up to 10 images');
|
||||
}
|
||||
|
||||
const urls: string[] = [];
|
||||
const uploads: SavedImageUpload[] = [];
|
||||
try {
|
||||
for (const file of files) {
|
||||
urls.push(await this.saveMediaFile('image', file));
|
||||
uploads.push(await this.saveResponsiveImageAsset('images', file));
|
||||
}
|
||||
return urls;
|
||||
return uploads;
|
||||
} catch (error) {
|
||||
await Promise.all(urls.map((url) => this.deleteManagedPostMedia(url)));
|
||||
await Promise.all(uploads.map((upload) => this.deleteSavedImageAsset(upload)));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveResponsiveImageAsset(
|
||||
folder: 'images' | 'thumbnails',
|
||||
file: UploadedImageFile,
|
||||
): Promise<SavedImageUpload> {
|
||||
this.validateMediaFile('image', file);
|
||||
const processed = await this.imageProcessingService.processForResponsiveDelivery(file);
|
||||
const groupName = `${folder.slice(0, -1)}-${new Types.ObjectId().toString()}`;
|
||||
const savedFiles = await this.storageService.saveFiles({
|
||||
folderSegments: ['posts', folder, groupName],
|
||||
files: processed.variants.map((variant) => ({
|
||||
relativePath: variant.relativePath,
|
||||
buffer: variant.buffer,
|
||||
contentType: variant.contentType,
|
||||
})),
|
||||
});
|
||||
|
||||
const variants = this.buildVariantSet(processed, savedFiles);
|
||||
return {
|
||||
primaryUrl: this.resolvePrimaryVariantUrl(variants, processed.primaryVariantName),
|
||||
variants,
|
||||
};
|
||||
}
|
||||
|
||||
private buildVariantSet(
|
||||
processed: Awaited<ReturnType<ImageProcessingService['processForResponsiveDelivery']>>,
|
||||
savedFiles: Record<string, string>,
|
||||
): PostMediaVariantSet {
|
||||
const byName = new Map(
|
||||
processed.variants.map((variant) => [variant.name, savedFiles[variant.relativePath] ?? '']),
|
||||
);
|
||||
const originalUrl = byName.get('original') ?? '';
|
||||
const lowUrl = byName.get('low') ?? originalUrl;
|
||||
const mediumUrl = byName.get('medium') ?? byName.get('high') ?? lowUrl;
|
||||
const highUrl = byName.get('high') ?? mediumUrl ?? lowUrl;
|
||||
|
||||
return {
|
||||
originalUrl,
|
||||
lowUrl,
|
||||
mediumUrl,
|
||||
highUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private resolvePrimaryVariantUrl(
|
||||
variants: PostMediaVariantSet,
|
||||
preferredVariantName: 'original' | 'low' | 'medium' | 'high',
|
||||
): string {
|
||||
const candidates =
|
||||
preferredVariantName === 'low'
|
||||
? [variants.lowUrl, variants.mediumUrl, variants.highUrl, variants.originalUrl]
|
||||
: preferredVariantName === 'high'
|
||||
? [variants.highUrl, variants.mediumUrl, variants.lowUrl, variants.originalUrl]
|
||||
: preferredVariantName === 'original'
|
||||
? [variants.originalUrl, variants.highUrl, variants.mediumUrl, variants.lowUrl]
|
||||
: [variants.mediumUrl, variants.highUrl, variants.lowUrl, variants.originalUrl];
|
||||
|
||||
return candidates.find((candidate) => !!candidate) ?? '';
|
||||
}
|
||||
|
||||
private buildSavedImageAssets(
|
||||
imageUrls: string[],
|
||||
imageVariants: PostMediaVariantSet[] = [],
|
||||
): SavedImageUpload[] {
|
||||
return imageUrls.map((primaryUrl, index) => ({
|
||||
primaryUrl,
|
||||
variants: imageVariants[index] ?? this.buildFlatVariantSet(primaryUrl),
|
||||
}));
|
||||
}
|
||||
|
||||
private buildFlatVariantSet(primaryUrl: string): PostMediaVariantSet {
|
||||
return {
|
||||
originalUrl: primaryUrl,
|
||||
lowUrl: primaryUrl,
|
||||
mediumUrl: primaryUrl,
|
||||
highUrl: primaryUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteSavedImageAsset(upload: SavedImageUpload): Promise<void> {
|
||||
const anchorUrl =
|
||||
upload.variants.mediumUrl ||
|
||||
upload.variants.highUrl ||
|
||||
upload.variants.lowUrl ||
|
||||
upload.variants.originalUrl ||
|
||||
upload.primaryUrl;
|
||||
|
||||
const hasManagedVariantGroup = [
|
||||
upload.variants.originalUrl,
|
||||
upload.variants.lowUrl,
|
||||
upload.variants.mediumUrl,
|
||||
upload.variants.highUrl,
|
||||
].some((url) => !!url && url !== upload.primaryUrl);
|
||||
|
||||
if (anchorUrl && hasManagedVariantGroup) {
|
||||
await this.storageService.deleteContainingDirectory(anchorUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteManagedPostMedia(upload.primaryUrl);
|
||||
}
|
||||
|
||||
private async deleteThumbnailAsset(
|
||||
thumbnailUrl: string,
|
||||
thumbnailVariants: PostMediaVariantSet | null,
|
||||
): Promise<void> {
|
||||
if (!thumbnailUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorUrl =
|
||||
thumbnailVariants?.mediumUrl ||
|
||||
thumbnailVariants?.highUrl ||
|
||||
thumbnailVariants?.lowUrl ||
|
||||
thumbnailVariants?.originalUrl ||
|
||||
'';
|
||||
|
||||
const hasManagedVariantGroup = !!thumbnailVariants &&
|
||||
[
|
||||
thumbnailVariants.originalUrl,
|
||||
thumbnailVariants.lowUrl,
|
||||
thumbnailVariants.mediumUrl,
|
||||
thumbnailVariants.highUrl,
|
||||
].some((url) => !!url && url !== thumbnailUrl);
|
||||
|
||||
if (anchorUrl && hasManagedVariantGroup) {
|
||||
await this.storageService.deleteContainingDirectory(anchorUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteManagedPostMedia(thumbnailUrl);
|
||||
}
|
||||
|
||||
private async deleteManagedPostMedia(fileUrl: string): Promise<void> {
|
||||
await this.storageService.deleteFile(fileUrl);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Prop, Schema, SchemaFactory, raw } from '@nestjs/mongoose';
|
||||
import { HydratedDocument, Types } from 'mongoose';
|
||||
import { ModerationStatus } from '../../../common/enums/moderation-status.enum';
|
||||
import { PostType } from '../../../common/enums/post-type.enum';
|
||||
import { PostVisibility } from '../../../common/enums/post-visibility.enum';
|
||||
import {
|
||||
resolveManagedFileUrl,
|
||||
resolveManagedFileUrlRecord,
|
||||
resolveManagedFileUrlRecords,
|
||||
resolveManagedFileUrls,
|
||||
} from '../../../common/utils/public-url.util';
|
||||
import { User } from '../../users/schemas/user.schema';
|
||||
|
||||
export type PostDocument = HydratedDocument<Post>;
|
||||
|
||||
export type PostMediaVariantSet = {
|
||||
originalUrl: string;
|
||||
lowUrl: string;
|
||||
mediumUrl: string;
|
||||
highUrl: string;
|
||||
};
|
||||
|
||||
const mediaVariantSetSchema = raw({
|
||||
originalUrl: { type: String, default: '' },
|
||||
lowUrl: { type: String, default: '' },
|
||||
mediumUrl: { type: String, default: '' },
|
||||
highUrl: { type: String, default: '' },
|
||||
});
|
||||
|
||||
@Schema({ timestamps: true, versionKey: false })
|
||||
export class Post {
|
||||
@Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true })
|
||||
@@ -34,6 +50,9 @@ export class Post {
|
||||
@Prop({ default: '' })
|
||||
thumbnailUrl!: string;
|
||||
|
||||
@Prop({ type: mediaVariantSetSchema, default: null })
|
||||
thumbnailVariants!: PostMediaVariantSet | null;
|
||||
|
||||
@Prop({ default: '', trim: true, maxlength: 80 })
|
||||
style!: string;
|
||||
|
||||
@@ -49,6 +68,9 @@ export class Post {
|
||||
@Prop({ type: [String], default: [] })
|
||||
imageUrls!: string[];
|
||||
|
||||
@Prop({ type: [mediaVariantSetSchema], default: [] })
|
||||
imageVariants!: PostMediaVariantSet[];
|
||||
|
||||
@Prop({ type: [Types.ObjectId], ref: User.name, default: [], index: true })
|
||||
taggedUserIds!: Types.ObjectId[];
|
||||
|
||||
@@ -136,10 +158,12 @@ PostSchema.index({
|
||||
|
||||
const transformManagedPostFiles = (_doc: unknown, ret: any) => {
|
||||
ret.imageUrls = resolveManagedFileUrls(ret.imageUrls);
|
||||
ret.imageVariants = resolveManagedFileUrlRecords(ret.imageVariants);
|
||||
ret.videoUrl = resolveManagedFileUrl(ret.videoUrl);
|
||||
ret.hlsUrl = resolveManagedFileUrl(ret.hlsUrl);
|
||||
ret.audioUrl = resolveManagedFileUrl(ret.audioUrl);
|
||||
ret.thumbnailUrl = resolveManagedFileUrl(ret.thumbnailUrl);
|
||||
ret.thumbnailVariants = resolveManagedFileUrlRecord(ret.thumbnailVariants);
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم