diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cb97f5..f492b6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,15 @@ name: ci on: -- pull_request -- push + push: + branches: + - master + - '1.0' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' jobs: test: @@ -10,123 +17,20 @@ jobs: strategy: matrix: name: - - Node.js 0.8 - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 5.x - - Node.js 6.x - - Node.js 7.x - - Node.js 8.x - - Node.js 9.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - Node.js 18.x - - Node.js 19.x - Node.js 20.x - - Node.js 21.x - Node.js 22.x - - include: - - name: Node.js 0.8 - node-version: "0.8" - npm-i: mocha@2.5.3 supertest@1.1.0 - npm-rm: nyc - - - name: Node.js 0.10 - node-version: "0.10" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 0.12 - node-version: "0.12" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 1.x - node-version: "1.8" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 2.x - node-version: "2.5" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 3.x - node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 4.x - node-version: "4.9" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 5.x - node-version: "5.12" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 6.x - node-version: "6.17" - npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 - - - name: Node.js 7.x - node-version: "7.10" - npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 - - - name: Node.js 8.x - node-version: "8.16" - npm-i: mocha@7.2.0 nyc@14.1.1 - - - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.2.0 nyc@14.1.1 - - - name: Node.js 10.x - node-version: "10.24" - npm-i: mocha@8.4.0 - - - name: Node.js 11.x - node-version: "11.15" - npm-i: mocha@8.4.0 - - - name: Node.js 12.x - node-version: "12.22" - - - name: Node.js 13.x - node-version: "13.14" - - - name: Node.js 14.x - node-version: "14.19" - - - name: Node.js 15.x - node-version: "15.14" - - - name: Node.js 16.x - node-version: "16.14" - - - name: Node.js 17.x - node-version: "17.7" + include: - name: Node.js 18.x - node-version: "18.14" - - - name: Node.js 19.x - node-version: "19.6" + node-version: "18" - name: Node.js 20.x - node-version: "20.12" + node-version: "20" - - name: Node.js 21.x - node-version: "21.7" - - name: Node.js 22.x - node-version: "22.0" - + node-version: "22" + steps: - uses: actions/checkout@v4 @@ -134,41 +38,11 @@ jobs: shell: bash -eo pipefail -l {0} run: | nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - nvm install --alias=npm 0.10 - nvm use ${{ matrix.node-version }} - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - npm config set strict-ssl false - fi dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - name: Configure npm run: | - if [[ "$(npm config get package-lock)" == "true" ]]; then - npm config set package-lock false - else - npm config set shrinkwrap false - fi - - - name: Remove npm module(s) ${{ matrix.npm-rm }} - run: npm rm --silent --save-dev ${{ matrix.npm-rm }} - if: matrix.npm-rm != '' - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 8 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - sort -r | \ - xargs -n1 npm rm --silent --save-dev - fi + npm config set package-lock false - name: Install Node.js dependencies run: npm install diff --git a/HISTORY.md b/HISTORY.md index a739774..2f27fe2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,26 @@ +1.0.0-beta.2 / 2024-03-04 +========================= + + * Changes from 0.18.0 + +1.0.0-beta.1 / 2022-02-04 +========================= + + * Drop support for Node.js 0.8 + * Remove `hidden` option -- use `dotfiles` option + * Remove `from` alias to `root` -- use `root` directly + * Remove `send.etag()` -- use `etag` in `options` + * Remove `send.index()` -- use `index` in `options` + * Remove `send.maxage()` -- use `maxAge` in `options` + * Remove `send.root()` -- use `root` in `options` + * Use `mime-types` for file to content type mapping -- removed `send.mime` + * deps: debug@3.1.0 + - Add `DEBUG_HIDE_DATE` environment variable + - Change timer to per-namespace instead of global + - Change non-TTY date format + - Remove `DEBUG_FD` environment variable support + - Support 256 namespace colors + 0.18.0 / 2022-03-23 =================== diff --git a/README.md b/README.md index 3f04f57..531b885 100644 --- a/README.md +++ b/README.md @@ -133,15 +133,6 @@ The `SendStream` is an event emitter and will emit the following events: The `pipe` method is used to pipe the response into the Node.js HTTP response object, typically `send(req, path, options).pipe(res)`. -### .mime - -The `mime` export is the global instance of the -[`mime` npm module](https://www.npmjs.com/package/mime). - -This is used to configure the MIME types that are associated with file extensions -as well as other options for how to resolve the MIME type of a file (like the -default type to use for an unknown file extension). - ## Error-handling By default when no `error` listeners are present an automatic response will be @@ -210,20 +201,22 @@ server.listen(3000) ### Custom file types ```js +var extname = require('path').extname var http = require('http') var parseUrl = require('parseurl') var send = require('send') -// Default unknown types to text/plain -send.mime.default_type = 'text/plain' - -// Add a custom type -send.mime.define({ - 'application/x-my-type': ['x-mt', 'x-mtt'] -}) - var server = http.createServer(function onRequest (req, res) { send(req, parseUrl(req).pathname, { root: '/www/public' }) + .on('headers', function (res, path) { + switch (extname(path)) { + case '.x-mt': + case '.x-mtt': + // custom type for these extensions + res.setHeader('Content-Type', 'application/x-my-type') + break + } + }) .pipe(res) }) diff --git a/index.js b/index.js index 9df39ba..546c717 100644 --- a/index.js +++ b/index.js @@ -14,14 +14,13 @@ var createError = require('http-errors') var debug = require('debug')('send') -var deprecate = require('depd')('send') var destroy = require('destroy') var encodeUrl = require('encodeurl') var escapeHtml = require('escape-html') var etag = require('etag') var fresh = require('fresh') var fs = require('fs') -var mime = require('mime') +var mime = require('mime-types') var ms = require('ms') var onFinished = require('on-finished') var parseRange = require('range-parser') @@ -68,7 +67,6 @@ var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/ */ module.exports = send -module.exports.mime = mime /** * Return a `SendStream` for `req` and `path`. @@ -122,17 +120,6 @@ function SendStream (req, path, options) { throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"') } - this._hidden = Boolean(opts.hidden) - - if (opts.hidden !== undefined) { - deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead') - } - - // legacy support - if (opts.dotfiles === undefined) { - this._dotfiles = undefined - } - this._extensions = opts.extensions !== undefined ? normalizeList(opts.extensions, 'extensions option') : [] @@ -160,10 +147,6 @@ function SendStream (req, path, options) { this._root = opts.root ? resolve(opts.root) : null - - if (!this._root && opts.from) { - this.from(opts.from) - } } /** @@ -172,90 +155,6 @@ function SendStream (req, path, options) { util.inherits(SendStream, Stream) -/** - * Enable or disable etag generation. - * - * @param {Boolean} val - * @return {SendStream} - * @api public - */ - -SendStream.prototype.etag = deprecate.function(function etag (val) { - this._etag = Boolean(val) - debug('etag %s', this._etag) - return this -}, 'send.etag: pass etag as option') - -/** - * Enable or disable "hidden" (dot) files. - * - * @param {Boolean} path - * @return {SendStream} - * @api public - */ - -SendStream.prototype.hidden = deprecate.function(function hidden (val) { - this._hidden = Boolean(val) - this._dotfiles = undefined - debug('hidden %s', this._hidden) - return this -}, 'send.hidden: use dotfiles option') - -/** - * Set index `paths`, set to a falsy - * value to disable index support. - * - * @param {String|Boolean|Array} paths - * @return {SendStream} - * @api public - */ - -SendStream.prototype.index = deprecate.function(function index (paths) { - var index = !paths ? [] : normalizeList(paths, 'paths argument') - debug('index %o', paths) - this._index = index - return this -}, 'send.index: pass index as option') - -/** - * Set root `path`. - * - * @param {String} path - * @return {SendStream} - * @api public - */ - -SendStream.prototype.root = function root (path) { - this._root = resolve(String(path)) - debug('root %s', this._root) - return this -} - -SendStream.prototype.from = deprecate.function(SendStream.prototype.root, - 'send.from: pass root as option') - -SendStream.prototype.root = deprecate.function(SendStream.prototype.root, - 'send.root: pass root as option') - -/** - * Set max-age to `maxAge`. - * - * @param {Number} maxAge - * @return {SendStream} - * @api public - */ - -SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) { - this._maxage = typeof maxAge === 'string' - ? ms(maxAge) - : Number(maxAge) - this._maxage = !isNaN(this._maxage) - ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) - : 0 - debug('max-age %d', this._maxage) - return this -}, 'send.maxage: pass maxAge as option') - /** * Emit error with `status`. * @@ -559,17 +458,8 @@ SendStream.prototype.pipe = function pipe (res) { // dotfile handling if (containsDotFile(parts)) { - var access = this._dotfiles - - // legacy support - if (access === undefined) { - access = parts[parts.length - 1][0] === '.' - ? (this._hidden ? 'allow' : 'ignore') - : 'allow' - } - - debug('%s dotfile "%s"', access, path) - switch (access) { + debug('%s dotfile "%s"', this._dotfiles, path) + switch (this._dotfiles) { case 'allow': break case 'deny': @@ -608,7 +498,7 @@ SendStream.prototype.send = function send (path, stat) { var ranges = req.headers.range var offset = options.start || 0 - if (headersSent(res)) { + if (res.headersSent) { // impossible to send now this.headersAlreadySent() return @@ -827,17 +717,11 @@ SendStream.prototype.type = function type (path) { if (res.getHeader('Content-Type')) return - var type = mime.lookup(path) - - if (!type) { - debug('no content-type') - return - } - - var charset = mime.charsets.lookup(type) + var ext = extname(path) + var type = mime.contentType(ext) || 'application/octet-stream' debug('content-type %s', type) - res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : '')) + res.setHeader('Content-Type', type) } /** @@ -1019,7 +903,7 @@ function getHeaderNames (res) { /** * Determine if emitter has listeners of a given type. * - * The way to do this check is done three different ways in Node.js >= 0.8 + * The way to do this check is done three different ways in Node.js >= 0.10 * so this consolidates them into a minimal set using instance methods. * * @param {EventEmitter} emitter @@ -1036,20 +920,6 @@ function hasListeners (emitter, type) { return count > 0 } -/** - * Determine if the response headers have been sent. - * - * @param {object} res - * @returns {boolean} - * @private - */ - -function headersSent (res) { - return typeof res.headersSent !== 'boolean' - ? Boolean(res._header) - : res.headersSent -} - /** * Normalize the index option into an array. * diff --git a/package.json b/package.json index 7f269d5..bfba279 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "send", "description": "Better streaming static file server with Range and conditional-GET support", - "version": "0.18.0", + "version": "1.0.0-beta.2", "author": "TJ Holowaychuk ", "contributors": [ "Douglas Christopher Wilson ", @@ -16,22 +16,21 @@ "server" ], "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "http-errors": "^2.0.0", + "mime-types": "^2.1.35", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "devDependencies": { - "after": "0.8.2", + "after": "^0.8.2", "eslint": "7.32.0", "eslint-config-standard": "14.1.1", "eslint-plugin-import": "2.25.4", @@ -39,8 +38,8 @@ "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "5.2.0", "eslint-plugin-standard": "4.1.0", - "mocha": "9.2.2", - "nyc": "15.1.0", + "mocha": "^10.7.0", + "nyc": "^17.0.0", "supertest": "6.2.2" }, "files": [ @@ -51,12 +50,13 @@ "index.js" ], "engines": { - "node": ">= 0.8.0" + "node": ">= 0.10" }, "scripts": { "lint": "eslint .", "test": "mocha --check-leaks --reporter spec --bail", "test-ci": "nyc --reporter=lcov --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test" + "test-cov": "nyc --reporter=html --reporter=text npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" } } diff --git a/scripts/version-history.js b/scripts/version-history.js new file mode 100644 index 0000000..c58268a --- /dev/null +++ b/scripts/version-history.js @@ -0,0 +1,63 @@ +'use strict' + +var fs = require('fs') +var path = require('path') + +var HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') +var MD_HEADER_REGEXP = /^====*$/ +var VERSION = process.env.npm_package_version +var VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ + +var historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') + +if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { + console.error('Missing header in HISTORY.md') + process.exit(1) +} + +if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { + console.error('Missing placeholder version in HISTORY.md') + process.exit(1) +} + +if (historyFileLines[0].indexOf('x') !== -1) { + var versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') + + if (!versionCheckRegExp.test(VERSION)) { + console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) + process.exit(1) + } +} + +historyFileLines[0] = VERSION + ' / ' + getLocaleDate() +historyFileLines[1] = repeat('=', historyFileLines[0].length) + +fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) + +function getLocaleDate () { + var now = new Date() + + return zeroPad(now.getFullYear(), 4) + '-' + + zeroPad(now.getMonth() + 1, 2) + '-' + + zeroPad(now.getDate(), 2) +} + +function repeat (str, length) { + var out = '' + + for (var i = 0; i < length; i++) { + out += str + } + + return out +} + +function zeroPad (number, length) { + var num = number.toString() + + while (num.length < length) { + num = '0' + num + } + + return num +} diff --git a/test/send.js b/test/send.js index d419f8f..050e4b9 100644 --- a/test/send.js +++ b/test/send.js @@ -147,16 +147,23 @@ describe('send(file).pipe(res)', function () { it('should set Content-Type via mime map', function (done) { request(app) .get('/name.txt') - .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect(200, function (err) { if (err) return done(err) request(app) .get('/tobi.html') - .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Type', 'text/html; charset=utf-8') .expect(200, done) }) }) + it('should default Content-Type to octet-stream', function (done) { + request(app) + .get('/no_ext') + .expect('Content-Type', 'application/octet-stream') + .expect(200, done) + }) + it('should 404 if file disappears after stat, before open', function (done) { var app = http.createServer(function (req, res) { send(req, req.url, { root: 'test/fixtures' }) @@ -810,151 +817,6 @@ describe('send(file).pipe(res)', function () { .expect(416, done) }) }) - - describe('.etag()', function () { - it('should support disabling etags', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .etag(false) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect(shouldNotHaveHeader('ETag')) - .expect(200, done) - }) - }) - - describe('.from()', function () { - it('should set with deprecated from', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url) - .from(fixtures) - .pipe(res) - }) - - request(app) - .get('/pets/../name.txt') - .expect(200, 'tobi', done) - }) - }) - - describe('.hidden()', function () { - it('should default support sending hidden files', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .hidden(true) - .pipe(res) - }) - - request(app) - .get('/.hidden.txt') - .expect(200, 'secret', done) - }) - }) - - describe('.index()', function () { - it('should be configurable', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .index('tobi.html') - .pipe(res) - }) - - request(app) - .get('/') - .expect(200, '

tobi

', done) - }) - - it('should support disabling', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .index(false) - .pipe(res) - }) - - request(app) - .get('/pets/') - .expect(403, done) - }) - - it('should support fallbacks', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url, { root: fixtures }) - .index(['default.htm', 'index.html']) - .pipe(res) - }) - - request(app) - .get('/pets/') - .expect(200, fs.readFileSync(path.join(fixtures, 'pets', 'index.html'), 'utf8'), done) - }) - }) - - describe('.maxage()', function () { - it('should default to 0', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage(undefined) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=0', done) - }) - - it('should floor to integer', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage(1234) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=1', done) - }) - - it('should accept string', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage('30d') - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=2592000', done) - }) - - it('should max at 1 year', function (done) { - var app = http.createServer(function (req, res) { - send(req, 'test/fixtures/name.txt') - .maxage(Infinity) - .pipe(res) - }) - - request(app) - .get('/name.txt') - .expect('Cache-Control', 'public, max-age=31536000', done) - }) - }) - - describe('.root()', function () { - it('should set root', function (done) { - var app = http.createServer(function (req, res) { - send(req, req.url) - .root(fixtures) - .pipe(res) - }) - - request(app) - .get('/pets/../name.txt') - .expect(200, 'tobi', done) - }) - }) }) describe('send(file, options)', function () { @@ -1066,14 +928,6 @@ describe('send(file, options)', function () { }) }) - describe('from', function () { - it('should set with deprecated from', function (done) { - request(createServer({ from: fixtures })) - .get('/pets/../name.txt') - .expect(200, 'tobi', done) - }) - }) - describe('dotfiles', function () { it('should default to "ignore"', function (done) { request(createServer({ root: fixtures })) @@ -1081,10 +935,10 @@ describe('send(file, options)', function () { .expect(404, done) }) - it('should allow file within dotfile directory for back-compat', function (done) { + it('should ignore file within dotfile directory', function (done) { request(createServer({ root: fixtures })) .get('/.mine/name.txt') - .expect(200, /tobi/, done) + .expect(404, done) }) it('should reject bad value', function (done) { @@ -1234,20 +1088,6 @@ describe('send(file, options)', function () { }) }) - describe('hidden', function () { - it('should default to false', function (done) { - request(app) - .get('/.hidden.txt') - .expect(404, 'Not Found', done) - }) - - it('should default support sending hidden files', function (done) { - request(createServer({ hidden: true, root: fixtures })) - .get('/.hidden.txt') - .expect(200, 'secret', done) - }) - }) - describe('immutable', function () { it('should default to false', function (done) { request(createServer({ root: fixtures })) @@ -1459,40 +1299,6 @@ describe('send(file, options)', function () { }) }) -describe('send.mime', function () { - it('should be exposed', function () { - assert.ok(send.mime) - }) - - describe('.default_type', function () { - before(function () { - this.default_type = send.mime.default_type - }) - - afterEach(function () { - send.mime.default_type = this.default_type - }) - - it('should change the default type', function (done) { - send.mime.default_type = 'text/plain' - - request(createServer({ root: fixtures })) - .get('/no_ext') - .expect('Content-Type', 'text/plain; charset=UTF-8') - .expect(200, done) - }) - - it('should not add Content-Type for undefined default', function (done) { - send.mime.default_type = undefined - - request(createServer({ root: fixtures })) - .get('/no_ext') - .expect(shouldNotHaveHeader('Content-Type')) - .expect(200, done) - }) - }) -}) - function createServer (opts, fn) { return http.createServer(function onRequest (req, res) { try {