[READ-ONLY] Mirror of https://github.com/FoxxMD/logging. A typed, opinionated, batteries-included, Pino-based logging solution for backend TS/JS projects foxxmd.github.io/logging
child-logger logging logging-library nodejs pinojs typescript-library
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Simplify log path logic and add testing

* Simplify the logic for testing if a file or parent directories are writeable
* Add a test suite to verify file/dir access testing works as expected

#2

FoxxMD (Sep 11, 2024, 11:19 AM EDT) 7b7a7ac8 3c0e2074

+126 -34
+3 -3
src/destinations.ts
··· 8 8 import {destination, DestinationStream} from "pino"; 9 9 import {build, prettyFactory} from "pino-pretty" 10 10 import {PRETTY_OPTS_FILE, prettyOptsConsoleFactory, prettyOptsFileFactory} from "./pretty.js"; 11 - import {fileOrDirectoryIsWriteable, parseBool} from "./util.js"; 11 + import {pathIsWriteable, parseBool} from "./util.js"; 12 12 import path from "path"; 13 13 import {Transform, TransformCallback} from "node:stream"; 14 14 import pump from 'pump'; ··· 39 39 const testPath = typeof logPath === 'function' ? logPath() : logPath; 40 40 41 41 try { 42 - fileOrDirectoryIsWriteable(testPath); 42 + pathIsWriteable(testPath); 43 43 } catch (e: any) { 44 44 throw new Error('Cannot write logs to rotating file due to an error while trying to access the specified logging directory', {cause: e}); 45 45 } ··· 92 92 93 93 try { 94 94 const filePath = typeof logPathVal === 'function' ? logPathVal() : logPathVal; 95 - fileOrDirectoryIsWriteable(filePath); 95 + pathIsWriteable(filePath); 96 96 const dest = destination({dest: filePath, mkdir: true, sync: false}) 97 97 98 98 return {
+52 -31
src/util.ts
··· 2 2 import {accessSync, constants} from "fs"; 3 3 import process from "process"; 4 4 5 - export const fileOrDirectoryIsWriteable = (location: string) => { 5 + export const pathIsWriteable = (location: string) => { 6 6 const pathInfo = pathUtil.parse(location); 7 7 const isDir = pathInfo.ext === ''; 8 + 9 + // first try location directly 8 10 try { 9 - accessSync(location, constants.R_OK | constants.W_OK); 11 + testHumanAccess(location); 10 12 return true; 11 - } catch (err: any) { 12 - const {code} = err; 13 - if (code === 'ENOENT') { 14 - // walk up path and see if we can access parent directories 15 - let currPath = pathInfo; 16 - let parentOK = false; 17 - let accessError = null; 18 - while(currPath.dir !== '' && currPath.dir !== '/') { 19 - try { 20 - accessSync(currPath.dir, constants.R_OK | constants.W_OK); 21 - parentOK = true; 22 - break; 23 - } catch (e) { 24 - if (code !== 'ENOENT') { 25 - break; 26 - accessError = e; 27 - } 13 + } catch (e) { 14 + if(e.cause === undefined || e.cause.code !== 'ENOENT') { 15 + // no permissions to file/folder or some other error 16 + throw e; 17 + } 18 + } 19 + 20 + // now we'll try walking up all parent directories until we find either: 21 + // 22 + // one that exists and is writeable (OK! we can write all subdirectories) 23 + // one that exists and is NOT writeable (cannot create subdirectories) 24 + // root directory (oh no) 25 + // some other system error 26 + let currPath = pathInfo; 27 + try { 28 + while (currPath.dir !== '' && currPath.dir !== '/') { 29 + try { 30 + testHumanAccess(currPath.dir); 31 + return true; 32 + } catch (e) { 33 + if (e.cause === undefined || e.cause.code !== 'ENOENT') { 34 + throw e; 28 35 } 29 36 currPath = pathUtil.parse(currPath.dir); 30 37 } 31 - if(parentOK) { 32 - return true; 33 - } 34 - if (!accessError || accessError.code === 'EACCES') { 35 - // also can't access directory :( 36 - throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and application does not have permission to write to the parent directory`); 37 - } else { 38 - 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}); 39 - } 40 - } else if (code === 'EACCES') { 41 - throw new Error(`${isDir ? 'Directory' : 'File'} exists at ${location} but application does not have permission to write to it.`); 38 + } 39 + } catch (e) { 40 + if (e.cause?.code === 'EACCES') { 41 + // also can't access directory :( 42 + 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}); 42 43 } else { 43 - throw new Error(`${isDir ? 'Directory' : 'File'} exists at ${location} but application is unable to access it due to a system error`, {cause: err}); 44 + 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}); 45 + } 46 + } 47 + 48 + // walked all parent dirs without reaching any that existed until we go to top-level! 49 + throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location} and no parent directories existed all the way to root directory!`); 50 + } 51 + 52 + const testHumanAccess = (location: string) => { 53 + const pathInfo = pathUtil.parse(location); 54 + const isDir = pathInfo.ext === ''; 55 + try { 56 + accessSync(location, constants.R_OK | constants.W_OK); 57 + } catch (e) { 58 + switch(e.code) { 59 + case 'ENOENT': 60 + throw new Error(`No ${isDir ? 'directory' : 'file'} exists at ${location}`, {cause: e}); 61 + case 'EACCES': 62 + throw new Error(`Permission denied to ${isDir ? 'directory' : 'file'} at ${location}`, {cause: e}); 63 + default: 64 + throw new Error(`System error occurred while trying to access ${isDir ? 'directory' : 'file'} at ${location}`, {cause: e}); 44 65 } 45 66 } 46 67 }
+71
tests/io.test.ts
··· 1 + import {describe} from "mocha"; 2 + import {expect} from "chai"; 3 + import withLocalTmpDir from 'with-local-tmp-dir'; 4 + import {mkdir, writeFile, chmod, constants} from 'node:fs/promises'; 5 + import path from "path"; 6 + import { pathIsWriteable } from "../src/util.js"; 7 + 8 + describe('Path Access', function () { 9 + 10 + it(`does not throw when file is writeable`, async function () { 11 + await withLocalTmpDir(async () => { 12 + await writeFile(path.join(process.cwd(), 'test.log'), '' ); 13 + expect(() => pathIsWriteable(path.join(process.cwd(), 'test.log'))).to.not.throw(); 14 + }, {unsafeCleanup: true}); 15 + }); 16 + 17 + it(`does not throw when file does not exist but parent directories can be created`, async function () { 18 + await withLocalTmpDir(async () => { 19 + expect(() => pathIsWriteable(path.join(process.cwd(), 'test/subfolder/another/test.log'))).to.not.throw(); 20 + }, {unsafeCleanup: true}); 21 + }); 22 + 23 + it(`does not throw when directory is read-only but file is writable`, async function () { 24 + await withLocalTmpDir(async () => { 25 + await mkdir(path.join(process.cwd(), 'test')); 26 + await writeFile(path.join(process.cwd(), 'test/test.log'), ''); 27 + await chmod(path.join(process.cwd(), 'test'), '555'); 28 + expect(() => pathIsWriteable(path.join(process.cwd(), 'test/test.log'))).to.not.throw(); 29 + 30 + //cleanup 31 + await chmod(path.join(process.cwd(), 'test'), '777'); 32 + await chmod(path.join(process.cwd(), 'test/test.log'), '777'); 33 + }, {unsafeCleanup: true}); 34 + }); 35 + 36 + it(`throws when file is read-only`, async function () { 37 + await withLocalTmpDir(async () => { 38 + await mkdir(path.join(process.cwd(), 'test')); 39 + await writeFile(path.join(process.cwd(), 'test/test.log'), '', {mode: 333}); 40 + expect(() => pathIsWriteable(path.join(process.cwd(), 'test/test.log'))).to.throw(); 41 + 42 + //cleanup 43 + await chmod(path.join(process.cwd(), 'test'), '777'); 44 + await chmod(path.join(process.cwd(), 'test/test.log'), '777'); 45 + }, {unsafeCleanup: true}); 46 + }); 47 + 48 + it(`throws when directory is read-only`, async function () { 49 + await withLocalTmpDir(async () => { 50 + await mkdir(path.join(process.cwd(), 'test'), {mode: '444', recursive: true}); 51 + expect(() => pathIsWriteable(path.join(process.cwd(), 'test'))).to.throw(); 52 + 53 + //cleanup 54 + await chmod(path.join(process.cwd(), 'test'), '777'); 55 + }, {unsafeCleanup: true}); 56 + }); 57 + 58 + it(`throws when file does not exist and a parent directory is not accessible`, async function () { 59 + await withLocalTmpDir(async () => { 60 + await mkdir(path.join(process.cwd(), 'test'), {mode: '555'}); 61 + expect(() => pathIsWriteable(path.join(process.cwd(), 'test/subfolder/test.log'))).to.throw(); 62 + 63 + // cleanup 64 + await chmod(path.join(process.cwd(), 'test'), '777'); 65 + }, {unsafeCleanup: true}); 66 + }); 67 + 68 + it(`throws when currently walked parent dir is root dir`, async function () { 69 + expect(() => pathIsWriteable(path.join('/nonExistent/test.log'))).to.throw(); 70 + }); 71 + })