11import * as fs from 'fs' ;
22import path from 'path' ;
3- import { fromPath as convertPDFtoImageFromPath } from 'pdf2pic' ;
43import { Readable } from 'stream' ;
54import { pipeline } from 'stream/promises' ;
65import { withFile as withTmpFile } from 'tmp-promise' ;
@@ -19,7 +18,6 @@ import { type DBConnection } from '../../../../drizzle/db';
1918import { type ItemRaw } from '../../../../drizzle/types' ;
2019import { BaseLogger } from '../../../../logger' ;
2120import { MaybeUser , MinimalMember } from '../../../../types' ;
22- import { asDefined } from '../../../../utils/assertions' ;
2321import { AuthorizationService } from '../../../authorization' ;
2422import FileService from '../../../file/file.service' ;
2523import { 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
288327export default FileItemService ;
0 commit comments