Skip to content

Commit 09fa558

Browse files
authored
Graasp file import (#1807)
* refactor(file-service): refactor the item upload * feat(graasp-import): first version of graasp file import * fix(graasp-import): remove the item reordering * fix(graasp-import): correct the rebase * fix(graasp-import): correct the rebase * fix(graasp-import): correct tests * fix(graasp-import): correct test fixture
1 parent 1294374 commit 09fa558

8 files changed

Lines changed: 494 additions & 174 deletions

File tree

src/services/item/plugins/file/itemFile.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ const basePlugin: FastifyPluginAsyncTypebox<GraaspPluginFileOptions> = async (fa
148148
log,
149149
);
150150
} else {
151-
item = await fileItemService.upload(tx, member, {
151+
item = await fileItemService.uploadFileAndCreateItem(tx, member, {
152152
parentId,
153153
filename,
154154
mimetype,

src/services/item/plugins/file/itemFile.service.ts

Lines changed: 115 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as fs from 'fs';
22
import path from 'path';
3-
import { fromPath as convertPDFtoImageFromPath } from 'pdf2pic';
43
import { Readable } from 'stream';
54
import { pipeline } from 'stream/promises';
65
import { withFile as withTmpFile } from 'tmp-promise';
@@ -19,7 +18,6 @@ import { type DBConnection } from '../../../../drizzle/db';
1918
import { type ItemRaw } from '../../../../drizzle/types';
2019
import { BaseLogger } from '../../../../logger';
2120
import { MaybeUser, MinimalMember } from '../../../../types';
22-
import { asDefined } from '../../../../utils/assertions';
2321
import { AuthorizationService } from '../../../authorization';
2422
import FileService from '../../../file/file.service';
2523
import { UploadEmptyFileError } from '../../../file/utils/errors';
@@ -82,13 +80,17 @@ class FileItemService extends ItemService {
8280
this.storageService = storageService;
8381
}
8482

85-
public buildFilePath(extension: string = '') {
83+
private buildFilePath(extension: string = '') {
8684
// TODO: CHANGE ??
8785
const filepath = `${randomHexOf4()}/${randomHexOf4()}/${randomHexOf4()}-${Date.now()}${extension}`;
8886
return path.join('files', filepath);
8987
}
9088

91-
async upload(
89+
/**
90+
* Upload the file and create an item from the extracted file properties.
91+
* @returns The newly created item
92+
*/
93+
async uploadFileAndCreateItem(
9294
dbConnection: DBConnection,
9395
actor: MinimalMember,
9496
{
@@ -107,89 +109,84 @@ class FileItemService extends ItemService {
107109
previousItemId?: ItemRaw['id'];
108110
},
109111
) {
110-
const filepath = this.buildFilePath(getFileExtension(filename)); // parentId, filename
112+
// Create temporary file
113+
return await withTmpFile(async ({ path: tmpPath }) => {
114+
// Write the uploaded file to the temporary file
115+
await pipeline(stream, fs.createWriteStream(tmpPath));
111116

112-
// check member storage limit
113-
await this.storageService.checkRemainingStorage(dbConnection, actor);
114-
115-
return await withTmpFile(async ({ path }) => {
116-
// Write uploaded file to a temporary file
117-
await pipeline(stream, fs.createWriteStream(path));
118-
119-
// Content to be indexed for search
120-
let content = '';
121-
if (MimeTypes.isPdf(mimetype)) {
122-
content = await readPdfContent(path);
123-
}
124-
125-
// Upload to storage
126-
await this.fileService.upload(actor, {
127-
file: fs.createReadStream(path),
128-
filepath,
117+
// Upload the file
118+
const fileProperties = await this.uploadFile(dbConnection, actor, {
119+
filename,
120+
filepath: tmpPath,
129121
mimetype,
130122
});
131123

132-
const size = await this.fileService.getFileSize(actor, filepath);
124+
// Add thumbnails if the file is an image or a pdf
125+
const thumbnail = await this.itemThumbnailService.generateThumbnail(tmpPath, mimetype);
133126

134-
// throw for empty files
135-
if (!size) {
136-
await this.fileService.delete(filepath);
137-
throw new UploadEmptyFileError();
138-
}
139-
140-
// create item from file properties
141-
const name = filename.substring(0, MAX_ITEM_NAME_LENGTH);
142-
const fileProperties: FileItemProperties = {
143-
name: filename,
144-
path: filepath,
145-
mimetype,
146-
size,
147-
content,
148-
};
149-
const item = {
150-
name,
127+
// Create item from file properties
128+
return await this.createItemFromFileProperties(dbConnection, actor, {
151129
description,
152-
type: ItemType.FILE,
153-
extra: {
154-
[ItemType.FILE]: fileProperties,
155-
},
156-
creator: actor,
157-
};
158-
159-
const newItem = await super.post(dbConnection, actor, {
160-
item,
161130
parentId,
131+
filename,
132+
fileProperties,
162133
previousItemId,
134+
thumbnail,
163135
});
136+
});
137+
}
138+
139+
/**
140+
* Upload a file to the database and return the file item properties.
141+
* @returns The file item properties extracted from the provided file
142+
*/
143+
async uploadFile(
144+
dbConnection: DBConnection,
145+
actor: MinimalMember,
146+
{
147+
filename,
148+
filepath,
149+
mimetype,
150+
}: {
151+
filename: string;
152+
filepath: string;
153+
mimetype: string;
154+
},
155+
) {
156+
// Check member storage limit
157+
await this.storageService.checkRemainingStorage(dbConnection, actor);
164158

165-
// add thumbnails if image or pdf
166-
// allow failures
167-
try {
168-
if (MimeTypes.isImage(mimetype)) {
169-
await this.itemThumbnailService.upload(
170-
dbConnection,
171-
actor,
172-
newItem.id,
173-
fs.createReadStream(path),
174-
);
175-
} else if (MimeTypes.isPdf(mimetype)) {
176-
// Convert first page of PDF to image buffer and upload as thumbnail
177-
const outputImg = await convertPDFtoImageFromPath(path)(1, { responseType: 'buffer' });
178-
const buffer = asDefined(outputImg.buffer);
179-
await this.itemThumbnailService.upload(
180-
dbConnection,
181-
actor,
182-
newItem.id,
183-
Readable.from(buffer),
184-
);
185-
}
186-
} catch (e) {
187-
console.error(e);
188-
}
189-
190-
// retrieve item again since hasThumbnail might have changed
191-
return await this.itemRepository.getOneOrThrow(dbConnection, newItem.id);
159+
// Build the storage file path
160+
const storageFilepath = this.buildFilePath(getFileExtension(filename));
161+
162+
// Upload to storage
163+
await this.fileService.upload(actor, {
164+
file: fs.createReadStream(filepath),
165+
filepath: storageFilepath,
166+
mimetype,
192167
});
168+
169+
// Check the file size
170+
const size = await this.fileService.getFileSize(actor, storageFilepath);
171+
if (!size) {
172+
await this.fileService.delete(storageFilepath);
173+
throw new UploadEmptyFileError();
174+
}
175+
176+
// Content to be indexed for search
177+
let content = '';
178+
if (MimeTypes.isPdf(mimetype)) {
179+
content = await readPdfContent(filepath);
180+
}
181+
182+
// Return the file item properties
183+
return {
184+
name: filename,
185+
path: storageFilepath,
186+
mimetype,
187+
size,
188+
content,
189+
} as FileItemProperties;
193190
}
194191

195192
async getFile(
@@ -283,6 +280,48 @@ class FileItemService extends ItemService {
283280

284281
await super.patch(dbConnection, member, item.id, body);
285282
}
283+
284+
/**
285+
* Form and post a new item with properties derived from the file.
286+
*/
287+
private async createItemFromFileProperties(
288+
dbConnection: DBConnection,
289+
actor: MinimalMember,
290+
{
291+
description,
292+
parentId,
293+
filename,
294+
fileProperties,
295+
previousItemId,
296+
thumbnail,
297+
}: {
298+
description?: string;
299+
parentId?: string;
300+
filename: string;
301+
fileProperties: FileItemProperties;
302+
previousItemId?: ItemRaw['id'];
303+
thumbnail?: Readable;
304+
},
305+
) {
306+
const name = filename.substring(0, MAX_ITEM_NAME_LENGTH);
307+
308+
const item = {
309+
name,
310+
description,
311+
type: ItemType.FILE,
312+
extra: {
313+
[ItemType.FILE]: fileProperties,
314+
},
315+
creator: actor,
316+
};
317+
318+
return super.post(dbConnection, actor, {
319+
item,
320+
parentId,
321+
previousItemId,
322+
thumbnail,
323+
});
324+
}
286325
}
287326

288327
export default FileItemService;

src/services/item/plugins/importExport/errors.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,16 @@ export class InvalidItemTypeForDownloadError extends GraaspItemZipError {
5858
);
5959
}
6060
}
61+
62+
export class GraaspExportInvalidFileError extends GraaspItemZipError {
63+
constructor(data?: unknown) {
64+
super(
65+
{
66+
code: 'GPIZERR005',
67+
statusCode: StatusCodes.BAD_REQUEST,
68+
message: FAILURE_MESSAGES.GRAASP_EXPORT_FILE_ERROR,
69+
},
70+
data,
71+
);
72+
}
73+
}

0 commit comments

Comments
 (0)