|  | @@ -1,5 +1,6 @@
 | 
	
		
			
				|  |  | +Smalltalk current createPackage: 'FileServer'!
 | 
	
		
			
				|  |  |  Object subclass: #FileServer
 | 
	
		
			
				|  |  | -	instanceVariableNames: 'path http fs url port basePath util username password'
 | 
	
		
			
				|  |  | +	instanceVariableNames: 'path http fs url port basePath util username password fallbackPage'
 | 
	
		
			
				|  |  |  	package: 'FileServer'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer methodsFor: 'accessing'!
 | 
	
	
		
			
				|  | @@ -12,6 +13,18 @@ basePath: aString
 | 
	
		
			
				|  |  |  	basePath := aString
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +fallbackPage
 | 
	
		
			
				|  |  | +	^fallbackPage
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +fallbackPage: aString
 | 
	
		
			
				|  |  | +	fallbackPage := aString
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +password: aPassword
 | 
	
		
			
				|  |  | +	password := aPassword.
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  port
 | 
	
		
			
				|  |  |  	^port
 | 
	
		
			
				|  |  |  !
 | 
	
	
		
			
				|  | @@ -24,10 +37,6 @@ username: aUsername
 | 
	
		
			
				|  |  |  	username := aUsername.
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -password: aPassword
 | 
	
		
			
				|  |  | -	password := aPassword.
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  username: aUsername password: aPassword
 | 
	
		
			
				|  |  |  	username := aUsername.
 | 
	
		
			
				|  |  |  	password := aPassword.
 | 
	
	
		
			
				|  | @@ -35,6 +44,15 @@ username: aUsername password: aPassword
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer methodsFor: 'initialization'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +checkDirectoryLayout
 | 
	
		
			
				|  |  | +	(fs existsSync: self basePath, 'index.html') ifFalse: [
 | 
	
		
			
				|  |  | +		console warn: 'Warning: project directory does not contain index.html'].
 | 
	
		
			
				|  |  | +	(fs existsSync: self basePath, 'st') ifFalse: [
 | 
	
		
			
				|  |  | +		console warn: 'Warning: project directory is missing an "st" directory'].
 | 
	
		
			
				|  |  | +	(fs existsSync: self basePath, 'js') ifFalse: [
 | 
	
		
			
				|  |  | +		console warn: 'Warning: project directory is missing a "js" directory'].
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  initialize
 | 
	
		
			
				|  |  |  	super initialize.
 | 
	
		
			
				|  |  |  	path := self require: 'path'.
 | 
	
	
		
			
				|  | @@ -45,28 +63,11 @@ initialize
 | 
	
		
			
				|  |  |  	port := self class defaultPort.
 | 
	
		
			
				|  |  |  	username := nil.
 | 
	
		
			
				|  |  |  	password := nil.
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -checkDirectoryLayout
 | 
	
		
			
				|  |  | -	(fs existsSync: self basePath, 'index.html') ifFalse: [
 | 
	
		
			
				|  |  | -		console warn: 'Warning: project directory does not contain index.html'].
 | 
	
		
			
				|  |  | -	(fs existsSync: self basePath, 'st') ifFalse: [
 | 
	
		
			
				|  |  | -		console warn: 'Warning: project directory is missing an "st" directory'].
 | 
	
		
			
				|  |  | -	(fs existsSync: self basePath, 'js') ifFalse: [
 | 
	
		
			
				|  |  | -		console warn: 'Warning: project directory is missing a "js" directory'].
 | 
	
		
			
				|  |  | +	fallbackPage := nil.
 | 
	
		
			
				|  |  |  ! !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer methodsFor: 'private'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -require: aModuleString
 | 
	
		
			
				|  |  | -	"call to the require function"
 | 
	
		
			
				|  |  | -	^require value: aModuleString
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -writeData: data toFileNamed: aFilename
 | 
	
		
			
				|  |  | -	console log: aFilename
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  base64Decode: aString
 | 
	
		
			
				|  |  |  	<return (new Buffer(aString, 'base64').toString())>
 | 
	
		
			
				|  |  |  !
 | 
	
	
		
			
				|  | @@ -94,19 +95,19 @@ isAuthenticated: aRequest
 | 
	
		
			
				|  |  |  			ifTrue: [^true]
 | 
	
		
			
				|  |  |  			ifFalse: [^false]
 | 
	
		
			
				|  |  |  	].
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +require: aModuleString
 | 
	
		
			
				|  |  | +	"call to the require function"
 | 
	
		
			
				|  |  | +	^require value: aModuleString
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +writeData: data toFileNamed: aFilename
 | 
	
		
			
				|  |  | +	console log: aFilename
 | 
	
		
			
				|  |  |  ! !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer methodsFor: 'request handling'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -handleRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  | -	aRequest method = 'PUT'
 | 
	
		
			
				|  |  | -		ifTrue: [self handlePUTRequest: aRequest respondTo: aResponse].
 | 
	
		
			
				|  |  | -	aRequest method = 'GET'
 | 
	
		
			
				|  |  | -		ifTrue:[self handleGETRequest: aRequest respondTo: aResponse].
 | 
	
		
			
				|  |  | -	aRequest method = 'OPTIONS'
 | 
	
		
			
				|  |  | -		ifTrue:[self handleOPTIONSRequest: aRequest respondTo: aResponse]
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  handleGETRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  |  	| uri filename |
 | 
	
		
			
				|  |  |  	uri := (url parse: aRequest url) pathname.
 | 
	
	
		
			
				|  | @@ -117,6 +118,15 @@ handleGETRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  |  			ifTrue: [self respondFileNamed: filename to: aResponse]]
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +handleOPTIONSRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  | +	aResponse writeHead: 200 options: #{'Access-Control-Allow-Origin' -> '*'.
 | 
	
		
			
				|  |  | +					'Access-Control-Allow-Methods' -> 'GET, PUT, POST, DELETE, OPTIONS'.
 | 
	
		
			
				|  |  | +					'Access-Control-Allow-Headers' -> 'Content-Type, Accept'.
 | 
	
		
			
				|  |  | +					'Content-Length' -> 0.
 | 
	
		
			
				|  |  | +					'Access-Control-Max-Age' -> 10}.
 | 
	
		
			
				|  |  | +	aResponse end
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  handlePUTRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  |  	| file stream |
 | 
	
		
			
				|  |  |  	(self isAuthenticated: aRequest)
 | 
	
	
		
			
				|  | @@ -142,13 +152,26 @@ handlePUTRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  |  		stream writable ifTrue: [stream end]]
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -handleOPTIONSRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  | -	aResponse writeHead: 200 options: #{'Access-Control-Allow-Origin' -> '*'.
 | 
	
		
			
				|  |  | -					'Access-Control-Allow-Methods' -> 'GET, PUT, POST, DELETE, OPTIONS'.
 | 
	
		
			
				|  |  | -					'Access-Control-Allow-Headers' -> 'Content-Type, Accept'.
 | 
	
		
			
				|  |  | -					'Content-Length' -> 0.
 | 
	
		
			
				|  |  | -					'Access-Control-Max-Age' -> 10}.
 | 
	
		
			
				|  |  | -	aResponse end
 | 
	
		
			
				|  |  | +handleRequest: aRequest respondTo: aResponse
 | 
	
		
			
				|  |  | +	aRequest method = 'PUT'
 | 
	
		
			
				|  |  | +		ifTrue: [self handlePUTRequest: aRequest respondTo: aResponse].
 | 
	
		
			
				|  |  | +	aRequest method = 'GET'
 | 
	
		
			
				|  |  | +		ifTrue:[self handleGETRequest: aRequest respondTo: aResponse].
 | 
	
		
			
				|  |  | +	aRequest method = 'OPTIONS'
 | 
	
		
			
				|  |  | +		ifTrue:[self handleOPTIONSRequest: aRequest respondTo: aResponse]
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +respondAuthenticationRequiredTo: aResponse
 | 
	
		
			
				|  |  | +	aResponse
 | 
	
		
			
				|  |  | +		writeHead: 401 options: #{'WWW-Authenticate' -> 'Basic realm="Secured Developer Area"'};
 | 
	
		
			
				|  |  | +		write: '<html><body>Authentication needed</body></html>';
 | 
	
		
			
				|  |  | +		end.
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +respondCreatedTo: aResponse
 | 
	
		
			
				|  |  | +	aResponse
 | 
	
		
			
				|  |  | +		writeHead: 201 options: #{'Content-Type' -> 'text/plain'. 'Access-Control-Allow-Origin' -> '*'};
 | 
	
		
			
				|  |  | +		end.
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  respondFileNamed: aFilename to: aResponse
 | 
	
	
		
			
				|  | @@ -180,33 +203,21 @@ respondInternalErrorTo: aResponse
 | 
	
		
			
				|  |  |  		end
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -respondAuthenticationRequiredTo: aResponse
 | 
	
		
			
				|  |  | +respondNotCreatedTo: aResponse
 | 
	
		
			
				|  |  |  	aResponse
 | 
	
		
			
				|  |  | -		writeHead: 401 options: #{'WWW-Authenticate' -> 'Basic realm="Secured Developer Area"'};
 | 
	
		
			
				|  |  | -		write: '<html><body>Authentication needed</body></html>';
 | 
	
		
			
				|  |  | +		writeHead: 400 options: #{'Content-Type' -> 'text/plain'};
 | 
	
		
			
				|  |  | +		write: 'File could not be created. Did you forget to create the st/js directories on the server?';
 | 
	
		
			
				|  |  |  		end.
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  respondNotFoundTo: aResponse
 | 
	
		
			
				|  |  | +	self fallbackPage isNil ifFalse: [^self respondFileNamed: self fallbackPage to: aResponse].
 | 
	
		
			
				|  |  |  	aResponse 
 | 
	
		
			
				|  |  |  		writeHead: 404 options: #{'Content-Type' -> 'text/plain'};
 | 
	
		
			
				|  |  |  		write: '404 Not found';
 | 
	
		
			
				|  |  |  		end
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -respondNotCreatedTo: aResponse
 | 
	
		
			
				|  |  | -	aResponse
 | 
	
		
			
				|  |  | -		writeHead: 400 options: #{'Content-Type' -> 'text/plain'};
 | 
	
		
			
				|  |  | -		write: 'File could not be created. Did you forget to create the st/js directories on the server?';
 | 
	
		
			
				|  |  | -		end.
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -respondCreatedTo: aResponse
 | 
	
		
			
				|  |  | -	aResponse
 | 
	
		
			
				|  |  | -		writeHead: 201 options: #{'Content-Type' -> 'text/plain'. 'Access-Control-Allow-Origin' -> '*'};
 | 
	
		
			
				|  |  | -		end.
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  respondOKTo: aResponse
 | 
	
		
			
				|  |  |  	aResponse
 | 
	
		
			
				|  |  |  		writeHead: 200 options: #{'Content-Type' -> 'text/plain'. 'Access-Control-Allow-Origin' -> '*'};
 | 
	
	
		
			
				|  | @@ -215,25 +226,30 @@ respondOKTo: aResponse
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer methodsFor: 'starting'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -startOn: aPort
 | 
	
		
			
				|  |  | -	self port: aPort.
 | 
	
		
			
				|  |  | -	self start
 | 
	
		
			
				|  |  | -!
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  start
 | 
	
		
			
				|  |  |  	(http createServer: [:request :response |
 | 
	
		
			
				|  |  |  	      self handleRequest: request respondTo: response])
 | 
	
		
			
				|  |  |  	      on: 'error' do: [:error | console log: 'Error starting server: ', error];
 | 
	
		
			
				|  |  |  	      on: 'listening' do: [console log: 'Starting file server on port ', self port asString];
 | 
	
		
			
				|  |  |  	      listen: self port.
 | 
	
		
			
				|  |  | +!
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +startOn: aPort
 | 
	
		
			
				|  |  | +	self port: aPort.
 | 
	
		
			
				|  |  | +	self start
 | 
	
		
			
				|  |  |  ! !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  FileServer class instanceVariableNames: 'mimeTypes'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer class methodsFor: 'accessing'!
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -defaultPort
 | 
	
		
			
				|  |  | -	^4000
 | 
	
		
			
				|  |  | +commandLineActions
 | 
	
		
			
				|  |  | +	^#{
 | 
	
		
			
				|  |  | +		'-p' -> [:fileServer :value | fileServer port: value].
 | 
	
		
			
				|  |  | +		'--username' -> [:fileServer :value | fileServer username: value].
 | 
	
		
			
				|  |  | +		'--password' -> [:fileServer :value | fileServer password: value].
 | 
	
		
			
				|  |  | +		'--fallback-page' -> [:fileServer :value | fileServer fallbackPage: value]
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  defaultMimeTypes
 | 
	
	
		
			
				|  | @@ -650,20 +666,16 @@ defaultMimeTypes
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -mimeTypes
 | 
	
		
			
				|  |  | -	^mimeTypes ifNil: [mimeTypes := self defaultMimeTypes]
 | 
	
		
			
				|  |  | +defaultPort
 | 
	
		
			
				|  |  | +	^4000
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  mimeTypeFor: aString
 | 
	
		
			
				|  |  |  	^self mimeTypes at: (aString replace: '.*[\.]' with: '') ifAbsent: ['text/plain']
 | 
	
		
			
				|  |  |  !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -commandLineActions
 | 
	
		
			
				|  |  | -	^#{
 | 
	
		
			
				|  |  | -		'-p' -> [:fileServer :value | fileServer port: value].
 | 
	
		
			
				|  |  | -		'--username' -> [:fileServer :value | fileServer username: value].
 | 
	
		
			
				|  |  | -		'--password' -> [:fileServer :value | fileServer password: value]
 | 
	
		
			
				|  |  | -	}
 | 
	
		
			
				|  |  | +mimeTypes
 | 
	
		
			
				|  |  | +	^mimeTypes ifNil: [mimeTypes := self defaultMimeTypes]
 | 
	
		
			
				|  |  |  ! !
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  !FileServer class methodsFor: 'initialization'!
 |