index.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. var crypto = require('crypto')
  2. var fs = require('fs')
  3. var zlib = require('zlib')
  4. var path = require('path')
  5. var mime = require('mime-types')
  6. var compressible = require('compressible')
  7. var readDir = require('fs-readdir-recursive')
  8. var debug = require('debug')('koa-static-cache')
  9. module.exports = function staticCache(dir, options, files) {
  10. if (typeof dir === 'object') {
  11. files = options
  12. options = dir
  13. dir = null
  14. }
  15. options = options || {}
  16. options.prefix = (options.prefix || '').replace(/\/$/, '') + '/'
  17. files = files || options.files || Object.create(null)
  18. dir = dir || options.dir || process.cwd()
  19. var enableGzip = !!options.gzip
  20. readDir(dir).forEach(function (name) {
  21. name = name.replace(/\\/g, '/')
  22. var pathname = options.prefix + name
  23. var obj = files[pathname] = {}
  24. var filename = obj.path = path.join(dir, name)
  25. var stats = fs.statSync(filename)
  26. var buffer = fs.readFileSync(filename)
  27. obj.cacheControl = options.cacheControl
  28. obj.maxAge = options.maxAge || 0
  29. obj.type = obj.mime = mime.lookup(pathname) || 'application/octet-stream'
  30. obj.mtime = stats.mtime.toUTCString()
  31. obj.length = stats.size
  32. obj.md5 = crypto.createHash('md5').update(buffer).digest('base64')
  33. debug('file: ' + JSON.stringify(obj, null, 2))
  34. if (options.buffer)
  35. obj.buffer = buffer
  36. buffer = null
  37. })
  38. if (options.alias) {
  39. Object.keys(options.alias).forEach(function (key) {
  40. var value = options.alias[key]
  41. if (files[value]) {
  42. files[key] = files[value]
  43. debug('aliasing ' + value + ' as ' + key)
  44. }
  45. })
  46. }
  47. return function* staticCache(next) {
  48. var file = files[safeDecodeURIComponent(this.path)]
  49. if (!file)
  50. return yield* next
  51. switch (this.method) {
  52. case 'HEAD':
  53. case 'GET':
  54. this.status = 200
  55. if (enableGzip) this.vary('Accept-Encoding')
  56. if (!file.buffer) {
  57. var stats = yield stat(file.path)
  58. if (stats.mtime > new Date(file.mtime)) {
  59. file.mtime = stats.mtime.toUTCString()
  60. file.md5 = null
  61. file.length = stats.size
  62. }
  63. }
  64. this.response.lastModified = file.mtime
  65. if (file.md5) this.response.etag = file.md5
  66. if (this.fresh)
  67. return this.status = 304
  68. this.type = file.type
  69. this.length = file.zipBuffer ? file.zipBuffer.length : file.length
  70. this.set('Cache-Control', file.cacheControl || 'public, max-age=' + file.maxAge)
  71. if (file.md5) this.set('Content-MD5', file.md5)
  72. if (this.method === 'HEAD')
  73. return
  74. var acceptGzip = this.acceptsEncodings('gzip') === 'gzip';
  75. if (file.zipBuffer) {
  76. if (acceptGzip) {
  77. this.set('Content-Encoding', 'gzip')
  78. this.body = file.zipBuffer
  79. } else {
  80. this.body = file.buffer
  81. }
  82. return
  83. }
  84. var shouldGzip = enableGzip
  85. && file.length > 1024
  86. && acceptGzip
  87. && compressible(file.type)
  88. if (file.buffer) {
  89. if (shouldGzip) {
  90. file.zipBuffer = yield gzip(file.buffer)
  91. this.set('Content-Encoding', 'gzip')
  92. this.body = file.zipBuffer
  93. } else {
  94. this.body = file.buffer
  95. }
  96. return
  97. }
  98. var stream = fs.createReadStream(file.path)
  99. // update file hash
  100. if (!file.md5) {
  101. var hash = crypto.createHash('md5')
  102. stream.on('data', hash.update.bind(hash))
  103. stream.on('end', function () {
  104. file.md5 = hash.digest('base64')
  105. })
  106. }
  107. this.body = stream
  108. // enable gzip will remove content length
  109. if (shouldGzip) {
  110. this.remove('Content-Length')
  111. this.set('Content-Encoding', 'gzip')
  112. this.body = stream.pipe(zlib.createGzip())
  113. }
  114. return
  115. case 'OPTIONS':
  116. this.status = 204
  117. this.set('Allow', 'HEAD,GET,OPTIONS')
  118. return
  119. default:
  120. this.status = 405
  121. this.set('Allow', 'HEAD,GET,OPTIONS')
  122. return
  123. }
  124. }
  125. }
  126. var stat = function (file) {
  127. return function (done) {
  128. fs.stat(file, done)
  129. }
  130. }
  131. function gzip(buf) {
  132. return function (done) {
  133. zlib.gzip(buf, done)
  134. }
  135. }
  136. function safeDecodeURIComponent(text) {
  137. try {
  138. return decodeURIComponent(text);
  139. } catch (e) {
  140. return text;
  141. }
  142. }