1
+ import * as ftp from 'basic-ftp' ;
2
+ import 'multer' ;
3
+ import { makeId } from '@gitroom/nestjs-libraries/services/make.is' ;
4
+ import mime from 'mime-types' ;
5
+ // @ts -ignore
6
+ import { getExtension } from 'mime' ;
7
+ import { IUploadProvider } from './upload.interface' ;
8
+ import axios from 'axios' ;
9
+ import { Readable } from 'stream' ;
10
+
11
+ /**
12
+ * FTP storage provider for uploading files via FTP protocol
13
+ * Supports both FTP and FTPS (FTP over SSL/TLS)
14
+ */
15
+ export class FTPStorage implements IUploadProvider {
16
+ private _host : string ;
17
+ private _port : number ;
18
+ private _user : string ;
19
+ private _password : string ;
20
+ private _remotePath : string ;
21
+ private _publicUrl : string ;
22
+ private _secure : boolean ;
23
+ private _passiveMode : boolean ;
24
+
25
+ /**
26
+ * Initialize FTP storage provider
27
+ * @param host - FTP server hostname
28
+ * @param port - FTP server port (default: 21)
29
+ * @param user - FTP username
30
+ * @param password - FTP password
31
+ * @param remotePath - Remote directory path on FTP server
32
+ * @param publicUrl - Public URL where uploaded files will be accessible via HTTP
33
+ * @param secure - Use FTPS (FTP over SSL/TLS) (default: false)
34
+ * @param passiveMode - Use passive FTP mode (default: true)
35
+ */
36
+ constructor (
37
+ host : string ,
38
+ port : number = 21 ,
39
+ user : string ,
40
+ password : string ,
41
+ remotePath : string ,
42
+ publicUrl : string ,
43
+ secure : boolean = false ,
44
+ passiveMode : boolean = true
45
+ ) {
46
+ this . _host = host ;
47
+ this . _port = port ;
48
+ this . _user = user ;
49
+ this . _password = password ;
50
+ this . _remotePath = remotePath . endsWith ( '/' ) ? remotePath . slice ( 0 , - 1 ) : remotePath ;
51
+ this . _publicUrl = publicUrl . endsWith ( '/' ) ? publicUrl . slice ( 0 , - 1 ) : publicUrl ;
52
+ this . _secure = secure ;
53
+ this . _passiveMode = passiveMode ;
54
+ }
55
+
56
+ /**
57
+ * Create and configure FTP client connection
58
+ * @returns Promise<ftp.Client> - Configured FTP client
59
+ */
60
+ private async createFTPClient ( ) : Promise < ftp . Client > {
61
+ const client = new ftp . Client ( ) ;
62
+
63
+ try {
64
+ // Configure connection timeout and keep-alive
65
+ client . ftp . timeout = 30000 ; // 30 seconds timeout
66
+
67
+ await client . access ( {
68
+ host : this . _host ,
69
+ port : this . _port ,
70
+ user : this . _user ,
71
+ password : this . _password ,
72
+ secure : this . _secure ,
73
+ secureOptions : this . _secure ? { rejectUnauthorized : false } : undefined ,
74
+ } ) ;
75
+
76
+ // Set passive mode
77
+ client . ftp . pasv = this . _passiveMode ;
78
+
79
+ return client ;
80
+ } catch ( error ) {
81
+ client . close ( ) ;
82
+ throw new Error ( `Failed to connect to FTP server: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Generate date-based directory structure (YYYY/MM/DD)
88
+ * @returns string - Directory path
89
+ */
90
+ private generateDatePath ( ) : string {
91
+ const now = new Date ( ) ;
92
+ const year = now . getFullYear ( ) ;
93
+ const month = String ( now . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
94
+ const day = String ( now . getDate ( ) ) . padStart ( 2 , '0' ) ;
95
+ return `${ year } /${ month } /${ day } ` ;
96
+ }
97
+
98
+ /**
99
+ * Ensure directory exists on FTP server, creating if necessary
100
+ * @param client - FTP client
101
+ * @param dirPath - Directory path to create
102
+ */
103
+ private async ensureDirectory ( client : ftp . Client , dirPath : string ) : Promise < void > {
104
+ try {
105
+ await client . ensureDir ( dirPath ) ;
106
+ } catch ( error ) {
107
+ throw new Error ( `Failed to create directory ${ dirPath } : ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Upload a file from a URL via FTP
113
+ * @param path - URL of the file to upload
114
+ * @returns Promise<string> - The public URL where the uploaded file can be accessed
115
+ */
116
+ async uploadSimple ( path : string ) : Promise < string > {
117
+ let client : ftp . Client | null = null ;
118
+
119
+ try {
120
+ // Download file from URL
121
+ const response = await axios . get ( path , { responseType : 'arraybuffer' } ) ;
122
+ const contentType = response . headers [ 'content-type' ] || 'application/octet-stream' ;
123
+
124
+ const extension = getExtension ( contentType ) ;
125
+ if ( ! extension ) {
126
+ throw new Error ( `Unable to determine file extension for content type: ${ contentType } ` ) ;
127
+ }
128
+
129
+ // Generate file path
130
+ const datePath = this . generateDatePath ( ) ;
131
+ const id = makeId ( 10 ) ;
132
+ const fileName = `${ id } .${ extension } ` ;
133
+ const remoteDir = `${ this . _remotePath } /${ datePath } ` ;
134
+ const remoteFilePath = `${ remoteDir } /${ fileName } ` ;
135
+ const publicPath = `${ datePath } /${ fileName } ` ;
136
+
137
+ // Connect to FTP server
138
+ client = await this . createFTPClient ( ) ;
139
+
140
+ // Ensure directory exists
141
+ await this . ensureDirectory ( client , remoteDir ) ;
142
+
143
+ // Create readable stream from buffer
144
+ const stream = Readable . from ( response . data ) ;
145
+
146
+ // Upload file
147
+ await client . uploadFrom ( stream , remoteFilePath ) ;
148
+
149
+ return `${ this . _publicUrl } /${ publicPath } ` ;
150
+ } catch ( error ) {
151
+ console . error ( 'Error uploading file via FTP uploadSimple:' , error ) ;
152
+ throw new Error ( `Failed to upload file from ${ path } : ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
153
+ } finally {
154
+ if ( client ) {
155
+ client . close ( ) ;
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Upload a file from form data via FTP
162
+ * @param file - The uploaded file object
163
+ * @returns Promise<any> - File information including the public access URL
164
+ */
165
+ async uploadFile ( file : Express . Multer . File ) : Promise < any > {
166
+ let client : ftp . Client | null = null ;
167
+
168
+ try {
169
+ // Generate file path
170
+ const datePath = this . generateDatePath ( ) ;
171
+ const id = makeId ( 10 ) ;
172
+ const extension = mime . extension ( file . mimetype ) || 'bin' ;
173
+ const fileName = `${ id } .${ extension } ` ;
174
+ const remoteDir = `${ this . _remotePath } /${ datePath } ` ;
175
+ const remoteFilePath = `${ remoteDir } /${ fileName } ` ;
176
+ const publicPath = `${ datePath } /${ fileName } ` ;
177
+
178
+ // Connect to FTP server
179
+ client = await this . createFTPClient ( ) ;
180
+
181
+ // Ensure directory exists
182
+ await this . ensureDirectory ( client , remoteDir ) ;
183
+
184
+ // Create readable stream from buffer
185
+ const stream = Readable . from ( file . buffer ) ;
186
+
187
+ // Upload file
188
+ await client . uploadFrom ( stream , remoteFilePath ) ;
189
+
190
+ const publicUrl = `${ this . _publicUrl } /${ publicPath } ` ;
191
+
192
+ return {
193
+ filename : fileName ,
194
+ mimetype : file . mimetype ,
195
+ size : file . size ,
196
+ buffer : file . buffer ,
197
+ originalname : fileName ,
198
+ fieldname : 'file' ,
199
+ path : publicUrl ,
200
+ destination : publicUrl ,
201
+ encoding : '7bit' ,
202
+ stream : file . buffer as any ,
203
+ } ;
204
+ } catch ( error ) {
205
+ console . error ( 'Error uploading file via FTP uploadFile:' , error ) ;
206
+ throw new Error ( `Failed to upload file ${ file . originalname } : ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
207
+ } finally {
208
+ if ( client ) {
209
+ client . close ( ) ;
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Remove a file from FTP storage
216
+ * @param filePath - The public URL of the file to remove
217
+ * @returns Promise<void>
218
+ */
219
+ async removeFile ( filePath : string ) : Promise < void > {
220
+ let client : ftp . Client | null = null ;
221
+
222
+ try {
223
+ // Extract relative path from public URL
224
+ const relativePath = filePath . replace ( this . _publicUrl + '/' , '' ) ;
225
+ const remoteFilePath = `${ this . _remotePath } /${ relativePath } ` ;
226
+
227
+ // Connect to FTP server
228
+ client = await this . createFTPClient ( ) ;
229
+
230
+ // Remove file
231
+ await client . remove ( remoteFilePath ) ;
232
+ } catch ( error ) {
233
+ console . error ( 'Error removing file via FTP:' , error ) ;
234
+ // Don't throw error for file removal failures to avoid breaking the application
235
+ // Just log the error as the file might already be deleted or not exist
236
+ console . warn ( `Failed to remove file ${ filePath } : ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
237
+ } finally {
238
+ if ( client ) {
239
+ client . close ( ) ;
240
+ }
241
+ }
242
+ }
243
+ }
0 commit comments