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