file-cache.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // file-cache.js
  2. // This file contains the implementation of the file cache system used by the VWF nodeJS server.
  3. var fs = require( 'fs' ),
  4. mime = require( 'mime' ),
  5. zlib = require( 'zlib' );
  6. // Helper function to generate a hash for a string.
  7. function hash( str ) {
  8. return require( 'crypto' ).createHash( 'md5' ).update( str ).digest( "hex" );
  9. }
  10. function _FileCache( ) {
  11. this.files = [ ];
  12. this.enabled = true;
  13. this.clear = function ( ) {
  14. this.files.length = 0;
  15. };
  16. // Function for determining whether to treat file as binary or text
  17. // based on file extentsion.
  18. this.getDataType = function ( file ) {
  19. var type = file.substr( file.lastIndexOf( '.' ) + 1 ).toLowerCase( );
  20. if ( type === 'js' || type === 'html' || type === 'xml' || type === 'txt' || type === 'xhtml' || type === 'css' ) {
  21. return "utf8";
  22. }
  23. else return "binary";
  24. };
  25. // Function for getting a file from the file cache.
  26. // Asynchronous, returns file to the callback function.
  27. // Passes null to the callback function if there is no such file.
  28. this.getFile = function ( path, callback ) {
  29. //First, check if we have already served this file
  30. // and have it already cached, if so, return the
  31. // already cached file.
  32. for ( var i = 0; i < this.files.length; i++ ) {
  33. if( this.files[ i ].path == path ) {
  34. global.log( 'serving from cache: ' + path, 2 );
  35. callback( this.files[ i ] );
  36. return;
  37. }
  38. }
  39. // if got here, have no record of this file yet.
  40. var datatype = this.getDataType( path );
  41. var file = fs.readFileSync( path );
  42. var stats = fs.statSync( path );
  43. if ( file ) {
  44. // The file was not already in the cache, but does exist.
  45. // Gzip the file, which is an async process, and callback
  46. // with the gzipped file entry when finished.
  47. var self = this;
  48. zlib.gzip( file, function ( _, zippeddata ) {
  49. var newentry = { };
  50. newentry.path = path;
  51. newentry.data = file;
  52. newentry.stats = stats;
  53. newentry.zippeddata = zippeddata;
  54. newentry.contentlength = file.length;
  55. newentry.datatype = datatype;
  56. newentry.hash = hash( file );
  57. global.log( newentry.hash, 2 );
  58. global.log( 'loading into cache: ' + path, 2 );
  59. if ( self.enabled == true ) {
  60. self.files.push( newentry );
  61. var watcher = fs.watchFile( path, { }, function ( event, filename ) {
  62. global.log( this.entry.path + ' has changed on disk', 2 );
  63. self.files.splice( self.files.indexOf( this.entry ), 1 );
  64. fs.unwatchFile(path); //make sure not to create new watchers at every reload
  65. } );
  66. watcher.entry = newentry;
  67. }
  68. callback( newentry );
  69. return;
  70. } );
  71. // Just returning since we're waiting on the async gzip process.
  72. return;
  73. }
  74. // File was not in cache, and did not exists, return null.
  75. callback( null );
  76. };
  77. // Function to serve a file out of the filecache.
  78. this.ServeFile = function ( request, filename, response, URL ) {
  79. FileCache.getFile( filename, function ( file ) {
  80. if ( !file ) {
  81. response.writeHead( 500, {
  82. "Content-Type": "text/plain"
  83. } );
  84. response.write( 'file load error' + '\n');
  85. response.end( );
  86. return;
  87. }
  88. var type = mime.getType( filename );
  89. if ( request.headers[ 'if-none-match' ] === file.hash ) {
  90. response.writeHead( 304, {
  91. "Content-Type": type,
  92. "Last-Modified": encodeURIComponent(file.stats.mtime),
  93. "ETag": file.hash,
  94. "Cache-Control":"public; max-age=31536000" ,
  95. } );
  96. response.end( );
  97. return;
  98. }
  99. if ( request.headers[ 'accept-encoding' ] && request.headers[ 'accept-encoding' ].indexOf( 'gzip' ) >= 0 ) {
  100. response.writeHead( 200, {
  101. "Content-Type": type,
  102. "Last-Modified": encodeURIComponent(file.stats.mtime),
  103. "ETag": file.hash,
  104. "Cache-Control":"public; max-age=31536000" ,
  105. 'Content-Encoding': 'gzip'
  106. } );
  107. response.write(file.zippeddata, file.datatype);
  108. }
  109. else if ( request.headers['range'] ) {
  110. // To support the HTML 5 audio tag, the server needs to respond with the Content-Range in the header
  111. var range = request.headers['range'];
  112. range = range.split('=')[1];
  113. var ranges = range.split('-');
  114. var start = parseInt(ranges[0]);
  115. var end = parseInt(ranges[1]) || (file.contentlength - 1);
  116. var bytesheader = 'bytes ' + start + '-' + end + '/' + file.contentlength;
  117. response.writeHead(206, {
  118. "Content-Type": type,
  119. "Content-Length": (end - start) + 1,
  120. "Last-Modified": encodeURIComponent(file.stats.mtime),
  121. "ETag": file.hash,
  122. "Cache-Control": "public; max-age=31536000",
  123. "Accept-Ranges": "bytes",
  124. "Content-Range": bytesheader
  125. });
  126. var newdata = file.data.slice(start, end + 1);
  127. response.write(newdata, file.datatype);
  128. }
  129. else {
  130. response.writeHead( 200, {
  131. "Content-Type": type,
  132. "Last-Modified": encodeURIComponent(file.stats.mtime),
  133. "ETag": file.hash,
  134. "Cache-Control":"public; max-age=31536000"
  135. } );
  136. response.write( file.data, file.datatype );
  137. }
  138. response.end( );
  139. } );
  140. };
  141. }
  142. exports._FileCache = _FileCache;