Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/ci-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ on:

jobs:
test:
uses: hapijs/.github/.github/workflows/ci-plugin.yml@min-node-18-hapi-21
uses: hapijs/.github/.github/workflows/ci-plugin.yml@min-node-20-hapi-21
18 changes: 18 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import HapiEslintPlugin from '@hapi/eslint-plugin';

export default defineConfig([
...HapiEslintPlugin.configs.module,
{
languageOptions: {
globals: {
Crypto: 'off',
File: 'off'
}
}
},
globalIgnores([
'test/directory/*',
'test/file/*'
])
]);
4 changes: 2 additions & 2 deletions lib/etag.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Util = require('util');

const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const LruCache = require('lru-cache');
const { LRUCache } = require('lru-cache');


const internals = {
Expand Down Expand Up @@ -109,4 +109,4 @@ exports.apply = async function (response, stat) {
};


exports.Cache = LruCache;
exports.Cache = LRUCache;
47 changes: 16 additions & 31 deletions lib/fs.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
'use strict';

const Fs = require('fs');
const Util = require('util');
const Fs = require('fs/promises');

const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const Hoek = require('@hapi/hoek');


const internals = {
methods: {
promised: ['open', 'close', 'fstat', 'readdir'],
raw: ['createReadStream']
},
methods: ['open', 'readdir'],
notFound: new Set(['ENOENT', 'ENOTDIR'])
};


exports.File = class {

constructor(path) {

this.path = path;
this.fd = null;
this.handle = null;
}

async open(mode) {

Hoek.assert(this.fd === null);
Hoek.assert(this.handle === null);

try {
this.fd = await exports.open(this.path, mode);
this.handle = await exports.open(this.path, mode);
}
catch (err) {
const data = { path: this.path };
Expand All @@ -49,18 +46,18 @@ exports.File = class {

close() {

if (this.fd !== null) {
Bounce.background(exports.close(this.fd));
this.fd = null;
if (this.handle !== null) {
Bounce.background(this.handle.close());
this.handle = null;
}
}

async stat() {

Hoek.assert(this.fd !== null);
Hoek.assert(this.handle !== null);

try {
const stat = await exports.fstat(this.fd);
const stat = await this.handle.stat();

if (stat.isDirectory()) {
throw Boom.forbidden(null, { code: 'EISDIR', path: this.path });
Expand All @@ -69,7 +66,7 @@ exports.File = class {
return stat;
}
catch (err) {
this.close(this.fd);
this.close();

Bounce.rethrow(err, ['boom', 'system']);
throw Boom.boomify(err, { message: 'Failed to stat file', data: { path: this.path } });
Expand All @@ -84,26 +81,14 @@ exports.File = class {

createReadStream(options) {

Hoek.assert(this.fd !== null);

options = Object.assign({ fd: this.fd, start: 0 }, options);
Hoek.assert(this.handle !== null);

const stream = exports.createReadStream(this.path, options);

if (options.autoClose !== false) {
this.fd = null; // The stream now owns the fd
}

return stream;
return this.handle.createReadStream({ start: 0, ...options });
}
};

// Export Fs methods
// Export Fs methods to allow overriding

for (const method of internals.methods.raw) {
for (const method of internals.methods) {
exports[method] = Fs[method].bind(Fs);
}

for (const method of internals.methods.promised) {
exports[method] = Util.promisify(Fs[method]);
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@
"@hapi/bounce": "^3.0.1",
"@hapi/hoek": "^11.0.2",
"@hapi/validate": "^2.0.1",
"lru-cache": "^7.14.1"
"lru-cache": "^11.2.2"
},
"devDependencies": {
"@hapi/code": "^9.0.3",
"@hapi/eslint-plugin": "^7.0.0",
"@hapi/file": "^3.0.0",
"@hapi/hapi": "^21.3.0",
"@hapi/lab": "^26.0.0",
"@types/node": "^14.18.37",
"joi": "^17.8.3",
"typescript": "^4.9.5"
"@types/node": "^20.19.24",
"joi": "^18.0.1",
"typescript": "~5.9.3"
},
"scripts": {
"test": "lab -f -a @hapi/code -t 100 -L -Y",
Expand Down
19 changes: 14 additions & 5 deletions test/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -797,19 +797,28 @@ describe('directory', () => {

it('only stats the file system once when requesting a file', async () => {

const orig = InertFs.fstat;
let callCnt = 0;
InertFs.fstat = function (...args) {

callCnt++;
return orig.apply(InertFs, args);
const origOpen = InertFs.open;
InertFs.open = async function (...openArgs) {

const handle = await origOpen.call(this, ...openArgs);
const origStat = handle.stat;

handle.stat = function (...args) {

callCnt++;
return origStat.call(this, ...args);
};

return handle;
};

const server = await provisionServer();
server.route({ method: 'GET', path: '/directory/{path*}', handler: { directory: { path: './' } } });

const res = await server.inject('/directory/directory.js');
Fs.fstat = orig;
InertFs.open = origOpen;
expect(callCnt).to.equal(1);
expect(res.statusCode).to.equal(200);
expect(res.payload).to.contain('hapi');
Expand Down
2 changes: 1 addition & 1 deletion test/esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('import()', () => {

it('exposes all methods and classes as named imports', () => {

expect(Object.keys(Inert)).to.equal([
expect(Object.keys(Inert).filter((k) => k !== 'module.exports')).to.equal([
'default',
'plugin'
]);
Expand Down
89 changes: 46 additions & 43 deletions test/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ const { describe, it } = lab;
const expect = Code.expect;


internals.mockNextInertFs = function (method, handler) {

const origOpen = InertFs.open;
InertFs.open = async function (...openArgs) {

InertFs.open = origOpen;

if (method === 'open') {
return handler.call(this, origOpen.bind(this), ...openArgs);
}

const handle = await origOpen.call(this, ...openArgs);

const origMethod = handle[method];
handle[method] = function (...args) {

handle[method] = origMethod;
return handler.call(this, origMethod.bind(this), ...args);
};

return handle;
};
};


describe('file', () => {

describe('handler()', () => {
Expand Down Expand Up @@ -303,7 +328,7 @@ describe('file', () => {
const res = await server.inject('/filefn/index.js');
expect(res.statusCode).to.equal(200);
expect(res.payload).to.contain('Set correct confine value');
expect(res.headers['content-type']).to.equal('application/javascript; charset=utf-8');
expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8');
expect(res.headers['content-length']).to.exist();
});

Expand Down Expand Up @@ -617,20 +642,14 @@ describe('file', () => {
const filepath = Path.join(__dirname, '..', 'package.json');
server.route({ method: 'GET', path: '/file', handler: { file: filepath } });

// Prepare complicated mocking setup to fake an io error
// Prepare mocking setup to fake an io error

const orig = InertFs.createReadStream;
InertFs.createReadStream = function (path, options) {
internals.mockNextInertFs('createReadStream', function (orig, options) {

InertFs.createReadStream = orig;
process.nextTick(Fs.closeSync, this.fd);

process.nextTick(() => {

Fs.closeSync(options.fd);
});

return InertFs.createReadStream(path, options);
};
return orig(options);
});

const res = await server.inject('/file');
expect(res.statusCode).to.equal(500);
Expand All @@ -643,20 +662,14 @@ describe('file', () => {
const server = await provisionServer();
server.route({ method: 'GET', path: '/file', handler: { file: Path.join(__dirname, '..', 'package.json') } });

// Prepare complicated mocking setup to fake an io error
// Prepare mocking setup to fake an io error

const orig = InertFs.createReadStream;
InertFs.createReadStream = function (path, options) {
internals.mockNextInertFs('createReadStream', function (orig, options) {

InertFs.createReadStream = orig;
process.nextTick(Fs.closeSync, this.fd);

process.nextTick(() => {

Fs.closeSync(options.fd);
});

return InertFs.createReadStream(path, options);
};
return orig(options);
});

const first = server.inject('/file');
const second = server.inject('/file');
Expand Down Expand Up @@ -1070,13 +1083,10 @@ describe('file', () => {
const filename = File.uniqueFilename(Os.tmpdir()) + '.package.json';
Fs.writeFileSync(filename, 'data');

const orig = InertFs.fstat;
InertFs.fstat = function (fd) { // can return EIO error
internals.mockNextInertFs('stat', (orig) => { // can return EIO error

InertFs.fstat = orig;
throw new Error('failed');
};

});

const server = await provisionServer();
server.route({ method: 'GET', path: '/', handler: { file: { path: filename, confine: false } } });
Expand All @@ -1092,12 +1102,10 @@ describe('file', () => {
const filename = File.uniqueFilename(Os.tmpdir()) + '.package.json';
Fs.writeFileSync(filename, 'data');

const orig = InertFs.open;
InertFs.open = function () { // can return EMFILE error
internals.mockNextInertFs('open', () => { // can return EMFILE error

InertFs.open = orig;
throw new Error('failed');
};
});

const server = await provisionServer();
server.route({ method: 'GET', path: '/', handler: { file: { path: filename, confine: false } } });
Expand Down Expand Up @@ -1130,14 +1138,12 @@ describe('file', () => {
let didOpen = false;
const res1 = await server.inject('/');

const orig = InertFs.open;
InertFs.open = async function (path, mode) { // fake alternate permission error
internals.mockNextInertFs('open', async (orig, path, mode) => { // fake alternate permission error

InertFs.open = orig;
didOpen = true;

try {
return await InertFs.open(path, mode);
return await orig(path, mode);
}
catch (err) {
if (err.code === 'EACCES') {
Expand All @@ -1151,7 +1157,7 @@ describe('file', () => {

throw err;
}
};
});

const res2 = await server.inject('/');

Expand Down Expand Up @@ -1316,14 +1322,11 @@ describe('file', () => {
// Catch createReadStream options

let createOptions;
const orig = InertFs.createReadStream;
InertFs.createReadStream = function (path, options) {
internals.mockNextInertFs('createReadStream', (orig, options) => {

InertFs.createReadStream = orig;
createOptions = options;

return InertFs.createReadStream(path, options);
};
return orig(options);
});

const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=1-4', 'accept-encoding': 'gzip' } });
expect(res.statusCode).to.equal(206);
Expand Down
Loading