Skip to content

Commit 17be90d

Browse files
authored
feat: delete previous cover after updating (#266)
* feat: delete previous cover after updating * reafctore: some changes * rmoved log * fix tests, refactro condition
1 parent 9433672 commit 17be90d

File tree

11 files changed

+114
-5
lines changed

11 files changed

+114
-5
lines changed

src/domain/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,12 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe
7272
repositories.userSessionRepository
7373
);
7474
const editorToolsService = new EditorToolsService(repositories.editorToolsRepository);
75+
const fileUploaderService = new FileUploaderService(repositories.objectStorageRepository, repositories.fileRepository);
7576

7677
const sharedServices = {
7778
editorTools: editorToolsService,
7879
note: noteService,
80+
fileUploader: fileUploaderService,
7981
/**
8082
* @todo find a way how to resolve circular dependency
8183
*/
@@ -85,8 +87,6 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe
8587
const noteSettingsService = new NoteSettingsService(repositories.noteSettingsRepository, repositories.teamRepository, sharedServices);
8688
const aiService = new AIService(repositories.aiRepository);
8789

88-
const fileUploaderService = new FileUploaderService(repositories.objectStorageRepository, repositories.fileRepository);
89-
9090
return {
9191
fileUploaderService,
9292
noteService,

src/domain/service/fileUploader.service.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type FileRepository from '@repository/file.repository.js';
66
import type ObjectRepository from '@repository/object.repository.js';
77
import { DomainError } from '@domain/entities/DomainError.js';
88
import mime from 'mime';
9+
import { isEmpty } from '@infrastructure/utils/empty.js';
910

1011
/**
1112
* File data for upload
@@ -143,6 +144,34 @@ export default class FileUploaderService {
143144
return fileData;
144145
}
145146

147+
/**
148+
* Delete file
149+
* @param key - file key
150+
*/
151+
public async deleteFile(key: UploadedFile['key']): Promise<void> {
152+
const fileData = await this.fileRepository.getByKey(key);
153+
154+
if (isEmpty(fileData)) {
155+
throw new DomainError('File not found');
156+
}
157+
158+
/**
159+
* Define file type and bucket
160+
*/
161+
const fileType = this.defineFileType(fileData.location);
162+
const bucket = this.defineBucketByFileType(fileType);
163+
164+
/**
165+
* Delete file from object storage and database
166+
*/
167+
const isRemovedFromObjectStorage = await this.objectRepository.delete(key, bucket);
168+
const isRemovedFromDatabase = await this.fileRepository.deleteByKey(key);
169+
170+
if (isRemovedFromObjectStorage === false || isRemovedFromDatabase === false) {
171+
throw new DomainError('Cannot delete file');
172+
}
173+
}
174+
146175
/**
147176
* Define file type by location
148177
* @param location - file location

src/domain/service/noteSettings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type User from '@domain/entities/user.js';
99
import { createInvitationHash } from '@infrastructure/utils/invitationHash.js';
1010
import { DomainError } from '@domain/entities/DomainError.js';
1111
import type { SharedDomainMethods } from './shared/index.js';
12+
import { notEmpty } from '@infrastructure/utils/empty.js';
1213

1314
/**
1415
* Service responsible for Note Settings
@@ -107,6 +108,13 @@ export default class NoteSettingsService {
107108
throw new DomainError(`Note settings not found`);
108109
}
109110

111+
/**
112+
* In this case we need to remove previous cover
113+
*/
114+
if (notEmpty(data.cover) && notEmpty(noteSettings.cover)) {
115+
await this.shared.fileUploader.deleteFile(noteSettings.cover);
116+
}
117+
110118
return await this.noteSettingsRepository.patchNoteSettingsById(noteSettings.id, data);
111119
}
112120

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type UploadedFile from '@domain/entities/file.js';
2+
3+
/**
4+
* Which methods of Domain can be used by other domains
5+
* Uses to decouple domains from each other
6+
*/
7+
export default interface FileUploaderServiceSharedMethods {
8+
/**
9+
* Delete file
10+
* @param key - file key
11+
*/
12+
deleteFile: (key: UploadedFile['key']) => Promise<void>;
13+
}

src/domain/service/shared/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type EditorToolsServiceSharedMethods from './editorTools.js';
2+
import type FileUploaderServiceSharedMethods from './fileUploader.js';
23
import type NoteServiceSharedMethods from './note.js';
34

45
export type SharedDomainMethods = {
56
editorTools: EditorToolsServiceSharedMethods;
67

78
note: NoteServiceSharedMethods;
9+
10+
fileUploader: FileUploaderServiceSharedMethods;
811
};

src/presentation/http/router/noteSettings.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,6 @@ describe('NoteSettings API', () => {
480480
await global.db.insertNoteSetting({
481481
noteId: note.id,
482482
isPublic: true,
483-
cover: 'image.png',
484483
});
485484

486485
/** Create test team if user is in team */

src/repository/file.repository.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,13 @@ export default class FileRepository {
4040
public async getFileLocationByKey<T extends FileType>(type: T, key: UploadedFile['key']): Promise<FileLocationByType[T] | null> {
4141
return await this.storage.getFileLocationByKey(type, key);
4242
};
43+
44+
/**
45+
* Delete file by key
46+
* @param key - file unique key
47+
* @returns true if file deleted
48+
*/
49+
public async deleteByKey(key: UploadedFile['key']): Promise<boolean> {
50+
return await this.storage.delete(key);
51+
}
4352
}

src/repository/object.repository.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,13 @@ export default class ObjectStorageRepository {
3636
public async insert(objectData: Buffer, key: string, bucket: string): Promise<string | null> {
3737
return await this.storage.uploadFile(bucket, key, objectData);
3838
}
39+
40+
/**
41+
* Delete object
42+
* @param key - object key
43+
* @param bucket - bucket name
44+
*/
45+
public async delete(key: string, bucket: string): Promise<boolean> {
46+
return await this.storage.removeFile(bucket, key);
47+
}
3948
}

src/repository/storage/postgres/orm/sequelize/file.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,22 @@ export default class FileSequelizeStorage {
161161

162162
return res.location as FileLocationByType[T];
163163
}
164+
165+
/**
166+
* Delete file
167+
* @param key - file key
168+
* @returns true if file deleted
169+
*/
170+
public async delete(key: UploadedFile['key']): Promise<boolean> {
171+
const affectedRows = await this.model.destroy({
172+
where: {
173+
key,
174+
},
175+
});
176+
177+
/**
178+
* If file not found return false
179+
*/
180+
return affectedRows > 0;
181+
}
164182
}

src/repository/storage/s3/index.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getLogger } from '@infrastructure/logging/index.js';
2-
import { S3Client, GetObjectCommand, PutObjectCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
2+
import { S3Client, GetObjectCommand, PutObjectCommand, CreateBucketCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
33
import { Buffer } from 'buffer';
44
import { Readable } from 'stream';
55
import { streamToBuffer } from '@infrastructure/utils/streamToBuffer.js';
@@ -88,6 +88,27 @@ export class S3Storage {
8888
}
8989
}
9090

91+
/**
92+
* Remove file from bucket
93+
* @param bucket - bucket name
94+
* @param key - file key
95+
* @returns true if object was deleted
96+
*/
97+
public async removeFile(bucket: string, key: string): Promise<boolean> {
98+
try {
99+
await this.s3.send(new DeleteObjectCommand({
100+
Bucket: bucket,
101+
Key: key,
102+
}))
103+
104+
return true;
105+
} catch (error) {
106+
s3StorageLogger.error(error)
107+
108+
return false;
109+
}
110+
}
111+
91112
/**
92113
* Method to create bucket in object storage, return its location
93114
* @param name - bucket name

0 commit comments

Comments
 (0)