···88import {destination, DestinationStream} from "pino";
99import {build, prettyFactory} from "pino-pretty"
1010import {PRETTY_OPTS_FILE, prettyOptsConsoleFactory, prettyOptsFileFactory} from "./pretty.js";
1111-import {fileOrDirectoryIsWriteable, parseBool} from "./util.js";
1111+import {pathIsWriteable, parseBool} from "./util.js";
1212import path from "path";
1313import {Transform, TransformCallback} from "node:stream";
1414import pump from 'pump';
···3939 const testPath = typeof logPath === 'function' ? logPath() : logPath;
40404141 try {
4242- fileOrDirectoryIsWriteable(testPath);
4242+ pathIsWriteable(testPath);
4343 } catch (e: any) {
4444 throw new Error('Cannot write logs to rotating file due to an error while trying to access the specified logging directory', {cause: e});
4545 }
···92929393 try {
9494 const filePath = typeof logPathVal === 'function' ? logPathVal() : logPathVal;
9595- fileOrDirectoryIsWriteable(filePath);
9595+ pathIsWriteable(filePath);
9696 const dest = destination({dest: filePath, mkdir: true, sync: false})
97979898 return {
+52-31
src/util.ts
···22import {accessSync, constants} from "fs";
33import process from "process";
4455-export const fileOrDirectoryIsWriteable = (location: string) => {
55+export const pathIsWriteable = (location: string) => {
66 const pathInfo = pathUtil.parse(location);
77 const isDir = pathInfo.ext === '';
88+99+ // first try location directly
810 try {
99- accessSync(location, constants.R_OK | constants.W_OK);
1111+ testHumanAccess(location);
1012 return true;
1111- } catch (err: any) {
1212- const {code} = err;
1313- if (code === 'ENOENT') {
1414- // walk up path and see if we can access parent directories
1515- let currPath = pathInfo;
1616- let parentOK = false;
1717- let accessError = null;
1818- while(currPath.dir !== '' && currPath.dir !== '/') {
1919- try {
2020- accessSync(currPath.dir, constants.R_OK | constants.W_OK);
2121- parentOK = true;
2222- break;
2323- } catch (e) {
2424- if (code !== 'ENOENT') {
2525- break;
2626- accessError = e;
2727- }
1313+ } catch (e) {
1414+ if(e.cause === undefined || e.cause.code !== 'ENOENT') {
1515+ // no permissions to file/folder or some other error
1616+ throw e;
1717+ }
1818+ }
1919+2020+ // now we'll try walking up all parent directories until we find either:
2121+ //
2222+ // one that exists and is writeable (OK! we can write all subdirectories)
2323+ // one that exists and is NOT writeable (cannot create subdirectories)
2424+ // root directory (oh no)
2525+ // some other system error
2626+ let currPath = pathInfo;
2727+ try {
2828+ while (currPath.dir !== '' && currPath.dir !== '/') {
2929+ try {
3030+ testHumanAccess(currPath.dir);
3131+ return true;
3232+ } catch (e) {
3333+ if (e.cause === undefined || e.cause.code !== 'ENOENT') {
3434+ throw e;
2835 }
2936 currPath = pathUtil.parse(currPath.dir);
3037 }
3131- if(parentOK) {
3232- return true;
3333- }
3434- if (!accessError || accessError.code === 'EACCES') {
3535- // also can't access directory :(
3636- throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and application does not have permission to write to the parent directory`);
3737- } else {
3838- throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and application is unable to access the parent directory due to a system error`, {cause: accessError});
3939- }
4040- } else if (code === 'EACCES') {
4141- throw new Error(`${isDir ? 'Directory' : 'File'} exists at ${location} but application does not have permission to write to it.`);
3838+ }
3939+ } catch (e) {
4040+ if (e.cause?.code === 'EACCES') {
4141+ // also can't access directory :(
4242+ throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and application does not have read or write permissions to a parent directory`,{cause: e});
4243 } else {
4343- throw new Error(`${isDir ? 'Directory' : 'File'} exists at ${location} but application is unable to access it due to a system error`, {cause: err});
4444+ throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and application is unable to access a parent directory due to a system error`, {cause: e});
4545+ }
4646+ }
4747+4848+ // walked all parent dirs without reaching any that existed until we go to top-level!
4949+ throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and no parent directories existed all the way to root directory!`);
5050+}
5151+5252+const testHumanAccess = (location: string) => {
5353+ const pathInfo = pathUtil.parse(location);
5454+ const isDir = pathInfo.ext === '';
5555+ try {
5656+ accessSync(location, constants.R_OK | constants.W_OK);
5757+ } catch (e) {
5858+ switch(e.code) {
5959+ case 'ENOENT':
6060+ throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location}`, {cause: e});
6161+ case 'EACCES':
6262+ throw new Error(`Permission denied to ${isDir ? 'directory' : 'file'} at ${location}`, {cause: e});
6363+ default:
6464+ throw new Error(`System error occurred while trying to access ${isDir ? 'directory' : 'file'} at ${location}`, {cause: e});
4465 }
4566 }
4667}
+71
tests/io.test.ts
···11+import {describe} from "mocha";
22+import {expect} from "chai";
33+import withLocalTmpDir from 'with-local-tmp-dir';
44+import {mkdir, writeFile, chmod, constants} from 'node:fs/promises';
55+import path from "path";
66+import { pathIsWriteable } from "../src/util.js";
77+88+describe('Path Access', function () {
99+1010+ it(`does not throw when file is writeable`, async function () {
1111+ await withLocalTmpDir(async () => {
1212+ await writeFile(path.join(process.cwd(), 'test.log'), '' );
1313+ expect(() => pathIsWriteable(path.join(process.cwd(), 'test.log'))).to.not.throw();
1414+ }, {unsafeCleanup: true});
1515+ });
1616+1717+ it(`does not throw when file does not exist but parent directories can be created`, async function () {
1818+ await withLocalTmpDir(async () => {
1919+ expect(() => pathIsWriteable(path.join(process.cwd(), 'test/subfolder/another/test.log'))).to.not.throw();
2020+ }, {unsafeCleanup: true});
2121+ });
2222+2323+ it(`does not throw when directory is read-only but file is writable`, async function () {
2424+ await withLocalTmpDir(async () => {
2525+ await mkdir(path.join(process.cwd(), 'test'));
2626+ await writeFile(path.join(process.cwd(), 'test/test.log'), '');
2727+ await chmod(path.join(process.cwd(), 'test'), '555');
2828+ expect(() => pathIsWriteable(path.join(process.cwd(), 'test/test.log'))).to.not.throw();
2929+3030+ //cleanup
3131+ await chmod(path.join(process.cwd(), 'test'), '777');
3232+ await chmod(path.join(process.cwd(), 'test/test.log'), '777');
3333+ }, {unsafeCleanup: true});
3434+ });
3535+3636+ it(`throws when file is read-only`, async function () {
3737+ await withLocalTmpDir(async () => {
3838+ await mkdir(path.join(process.cwd(), 'test'));
3939+ await writeFile(path.join(process.cwd(), 'test/test.log'), '', {mode: 333});
4040+ expect(() => pathIsWriteable(path.join(process.cwd(), 'test/test.log'))).to.throw();
4141+4242+ //cleanup
4343+ await chmod(path.join(process.cwd(), 'test'), '777');
4444+ await chmod(path.join(process.cwd(), 'test/test.log'), '777');
4545+ }, {unsafeCleanup: true});
4646+ });
4747+4848+ it(`throws when directory is read-only`, async function () {
4949+ await withLocalTmpDir(async () => {
5050+ await mkdir(path.join(process.cwd(), 'test'), {mode: '444', recursive: true});
5151+ expect(() => pathIsWriteable(path.join(process.cwd(), 'test'))).to.throw();
5252+5353+ //cleanup
5454+ await chmod(path.join(process.cwd(), 'test'), '777');
5555+ }, {unsafeCleanup: true});
5656+ });
5757+5858+ it(`throws when file does not exist and a parent directory is not accessible`, async function () {
5959+ await withLocalTmpDir(async () => {
6060+ await mkdir(path.join(process.cwd(), 'test'), {mode: '555'});
6161+ expect(() => pathIsWriteable(path.join(process.cwd(), 'test/subfolder/test.log'))).to.throw();
6262+6363+ // cleanup
6464+ await chmod(path.join(process.cwd(), 'test'), '777');
6565+ }, {unsafeCleanup: true});
6666+ });
6767+6868+ it(`throws when currently walked parent dir is root dir`, async function () {
6969+ expect(() => pathIsWriteable(path.join('/nonExistent/test.log'))).to.throw();
7070+ });
7171+})