1
+ module . exports = function ( options ) {
2
+
3
+ var path = require ( 'path' ) ,
4
+ fs = require ( 'fs' ) ,
5
+ formidable = require ( 'formidable' ) ,
6
+ imageMagick = require ( 'imagemagick' ) ,
7
+ _ = require ( 'lodash' ) ,
8
+ // Since Node 0.8, .existsSync() moved from path to fs:
9
+ _existsSync = fs . existsSync || path . existsSync ,
10
+ utf8encode = function ( str ) {
11
+ return unescape ( encodeURIComponent ( str ) ) ;
12
+ } ,
13
+ nameCountRegexp = / (?: (?: \( ( [ \d ] + ) \) ) ? ( \. [ ^ . ] + ) ) ? $ / ,
14
+ nameCountFunc = function ( s , index , ext ) {
15
+ return ' (' + ( ( parseInt ( index , 10 ) || 0 ) + 1 ) + ')' + ( ext || '' ) ;
16
+ } ;
17
+
18
+ options = _ . extend ( {
19
+ tmpDir : '/tmp' ,
20
+ uploadDir : __dirname + '/public/files' ,
21
+ uploadUrl : '/files/' ,
22
+ maxPostSize : 11000000000 , // 11 GB
23
+ minFileSize : 1 ,
24
+ maxFileSize : 10000000000 , // 10 GB
25
+ acceptFileTypes : / .+ / i,
26
+ // Files not matched by this regular expression force a download dialog,
27
+ // to prevent executing any scripts in the context of the service domain:
28
+ safeFileTypes : / \. ( g i f | j p e ? g | p n g ) $ / i,
29
+ imageTypes : / \. ( g i f | j p e ? g | p n g ) $ / i,
30
+ imageVersions : {
31
+ /*
32
+ thumbnail: {
33
+ width: 80,
34
+ height: 80
35
+ }
36
+ */
37
+ } ,
38
+ accessControl : {
39
+ allowOrigin : '*' ,
40
+ allowMethods : 'OPTIONS, HEAD, GET, POST, PUT, DELETE'
41
+ }
42
+ } , options ) ;
43
+
44
+ var FileInfo = function ( file ) {
45
+ this . name = file . name ;
46
+ this . size = file . size ;
47
+ this . type = file . type ;
48
+ this . delete_type = 'DELETE' ;
49
+ } ;
50
+
51
+ FileInfo . prototype . validate = function ( ) {
52
+ if ( options . minFileSize && options . minFileSize > this . size ) {
53
+ this . error = 'File is too small' ;
54
+ } else if ( options . maxFileSize && options . maxFileSize < this . size ) {
55
+ this . error = 'File is too big' ;
56
+ } else if ( ! options . acceptFileTypes . test ( this . name ) ) {
57
+ this . error = 'Filetype not allowed' ;
58
+ }
59
+ return ! this . error ;
60
+ } ;
61
+
62
+ FileInfo . prototype . safeName = function ( ) {
63
+ // Prevent directory traversal and creating hidden system files:
64
+ this . name = path . basename ( this . name ) . replace ( / ^ \. + / , '' ) ;
65
+ // Prevent overwriting existing files:
66
+ while ( _existsSync ( options . uploadDir + '/' + this . name ) ) {
67
+ this . name = this . name . replace ( nameCountRegexp , nameCountFunc ) ;
68
+ }
69
+ } ;
70
+
71
+ FileInfo . prototype . initUrls = function ( req ) {
72
+ if ( ! this . error ) {
73
+ var that = this ,
74
+ baseUrl = ( options . ssl ? 'https:' : 'http:' ) +
75
+ '//' + req . headers . host ;
76
+ this . delete_url = baseUrl + req . originalUrl + '/' + encodeURIComponent ( this . name ) ;
77
+ this . url = baseUrl + options . uploadUrl + '/' + encodeURIComponent ( this . name ) ;
78
+ Object . keys ( options . imageVersions ) . forEach ( function ( version ) {
79
+ if ( _existsSync (
80
+ options . uploadDir + '/' + version + '/' + that . name
81
+ ) ) {
82
+ that [ version + '_url' ] = baseUrl + options . uploadUrl + '/' + version + '/' + encodeURIComponent ( that . name ) ;
83
+ }
84
+ } ) ;
85
+ }
86
+ } ;
87
+
88
+ var UploadHandler = function ( req , res , callback ) {
89
+ this . req = req ;
90
+ this . res = res ;
91
+ this . callback = callback ;
92
+ } ;
93
+
94
+ UploadHandler . prototype . noCache = function ( ) {
95
+ this . res . set ( {
96
+ 'Pragma' : 'no-cache' ,
97
+ 'Cache-Control' : 'no-store, no-cache, must-revalidate' ,
98
+ 'Content-Disposition' : 'inline; filename="files.json"'
99
+ } ) ;
100
+ } ;
101
+
102
+ UploadHandler . prototype . get = function ( ) {
103
+ var handler = this ,
104
+ files = [ ] ;
105
+ handler . noCache ( ) ;
106
+ fs . readdir ( options . uploadDir , function ( err , list ) {
107
+ _ . each ( list , function ( name ) {
108
+ var stats = fs . statSync ( options . uploadDir + '/' + name ) ,
109
+ fileInfo ;
110
+ if ( stats . isFile ( ) ) {
111
+ fileInfo = new FileInfo ( {
112
+ name : name ,
113
+ size : stats . size
114
+ } ) ;
115
+ fileInfo . initUrls ( handler . req ) ;
116
+ files . push ( fileInfo ) ;
117
+ }
118
+ } ) ;
119
+ handler . callback ( files ) ;
120
+ } ) ;
121
+ } ;
122
+
123
+ UploadHandler . prototype . post = function ( ) {
124
+
125
+ var handler = this ,
126
+ form = new formidable . IncomingForm ( ) ,
127
+ tmpFiles = [ ] ,
128
+ files = [ ] ,
129
+ map = { } ,
130
+ counter = 1 ,
131
+ redirect ,
132
+ finish = function ( ) {
133
+ if ( ! -- counter ) {
134
+ files . forEach ( function ( fileInfo ) {
135
+ fileInfo . initUrls ( handler . req ) ;
136
+ } ) ;
137
+ handler . callback ( files , redirect ) ;
138
+ }
139
+ } ;
140
+
141
+ handler . noCache ( ) ;
142
+
143
+ form . uploadDir = options . tmpDir ;
144
+ form
145
+ . on ( 'fileBegin' , function ( name , file ) {
146
+ tmpFiles . push ( file . path ) ;
147
+ var fileInfo = new FileInfo ( file , handler . req , true ) ;
148
+ fileInfo . safeName ( ) ;
149
+ map [ path . basename ( file . path ) ] = fileInfo ;
150
+ files . push ( fileInfo ) ;
151
+ } )
152
+ . on ( 'field' , function ( name , value ) {
153
+ if ( name === 'redirect' ) {
154
+ redirect = value ;
155
+ }
156
+ } )
157
+ . on ( 'file' , function ( name , file ) {
158
+ var fileInfo = map [ path . basename ( file . path ) ] ;
159
+ if ( _existsSync ( file . path ) ) {
160
+ fileInfo . size = file . size ;
161
+ if ( ! fileInfo . validate ( ) ) {
162
+ fs . unlink ( file . path ) ;
163
+ return ;
164
+ }
165
+ fs . renameSync ( file . path , options . uploadDir + '/' + fileInfo . name ) ;
166
+ if ( options . imageTypes . test ( fileInfo . name ) && _ . keys ( options . imageVersions ) . length ) {
167
+ Object . keys ( options . imageVersions ) . forEach ( function ( version ) {
168
+ if ( ! _existsSync ( options . uploadDir + '/' + version + '/' ) )
169
+ throw new Error ( options . uploadDir + '/' + version + '/' + ' not exists' ) ;
170
+ counter ++ ;
171
+ var opts = options . imageVersions [ version ] ;
172
+ imageMagick . resize ( {
173
+ width : opts . width ,
174
+ height : opts . height ,
175
+ srcPath : options . uploadDir + '/' + fileInfo . name ,
176
+ dstPath : options . uploadDir + '/' + version + '/' + fileInfo . name
177
+ } , finish ) ;
178
+ } ) ;
179
+ }
180
+ }
181
+ } )
182
+ . on ( 'aborted' , function ( ) {
183
+ tmpFiles . forEach ( function ( file ) {
184
+ fs . unlink ( file ) ;
185
+ } ) ;
186
+ } )
187
+ . on ( 'error' , function ( e ) {
188
+ console . log ( e ) ;
189
+ } )
190
+ . on ( 'progress' , function ( bytesReceived , bytesExpected ) {
191
+ if ( bytesReceived > options . maxPostSize )
192
+ handler . req . connection . destroy ( ) ;
193
+ } )
194
+ . on ( 'end' , finish )
195
+ . parse ( handler . req ) ;
196
+ } ;
197
+
198
+ UploadHandler . prototype . destroy = function ( ) {
199
+ var handler = this ,
200
+ fileName = path . basename ( decodeURIComponent ( this . req . url ) ) ;
201
+
202
+ fs . unlink ( options . uploadDir + '/' + fileName , function ( ex ) {
203
+ Object . keys ( options . imageVersions ) . forEach ( function ( version ) {
204
+ fs . unlink ( options . uploadDir + '/' + version + '/' + fileName ) ;
205
+ } ) ;
206
+ handler . callback ( ! ex ) ;
207
+ } ) ;
208
+ } ;
209
+
210
+ return function ( req , res , next ) {
211
+ res . set ( {
212
+ 'Access-Control-Allow-Origin' : options . accessControl . allowOrigin ,
213
+ 'Access-Control-Allow-Methods' : options . accessControl . allowMethods
214
+ } ) ;
215
+ var handler = new UploadHandler ( req , res , function ( result , redirect ) {
216
+ if ( redirect ) {
217
+ res . redirect ( redirect . replace ( / % s / , encodeURIComponent ( JSON . stringify ( result ) ) ) ) ;
218
+ } else {
219
+ res . set ( {
220
+ 'Content-Type' : req . headers . accept . indexOf ( 'application/json' ) !== - 1
221
+ ? 'application/json'
222
+ : 'text/plain'
223
+ } ) ;
224
+ res . json ( 200 , result ) ;
225
+ }
226
+ } ) ;
227
+ switch ( req . method ) {
228
+ case 'OPTIONS' :
229
+ res . end ( ) ;
230
+ break ;
231
+ case 'HEAD' :
232
+ case 'GET' :
233
+ handler . get ( ) ;
234
+ break ;
235
+ case 'POST' :
236
+ handler . post ( ) ;
237
+ break ;
238
+ case 'DELETE' :
239
+ handler . destroy ( ) ;
240
+ break ;
241
+ default :
242
+ res . send ( 405 ) ;
243
+ }
244
+
245
+ }
246
+
247
+ } ;
0 commit comments