Sag mir wo die Karabiner sind :)
This commit is contained in:
parent
14189a7e7a
commit
712ac9b004
@ -10,7 +10,7 @@
|
||||
"dev": "tsc -p . --watch",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"test:pdf": "node dist/index.js convert --input tests/e5dc.pdf -o tests/out/e5dc/ --startPage 3 --endPage 5",
|
||||
"test:pdf": "node dist/index.js convert --input tests/e5dc.pdf --output tests/out/e5dc/ --startPage 3 --endPage 5",
|
||||
"test:basic": "vitest run",
|
||||
"test:variables": "vitest run tests/cli/variables.test.ts"
|
||||
},
|
||||
|
||||
@ -3,67 +3,56 @@ import { describe, it, expect, vi, beforeEach, Mock, beforeAll } from 'vitest';
|
||||
// Import types first
|
||||
import type { ConvertCommandConfig } from '../../src/types.js';
|
||||
import type { Arguments } from 'yargs';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import path from 'path';
|
||||
// Remove Buffer import if readFile is no longer mocked directly
|
||||
// import { Buffer } from 'node:buffer';
|
||||
// Import path for constants only if needed, remove specific utils
|
||||
import path from 'path';
|
||||
|
||||
// --- Define Mock Functions ---
|
||||
const mockConvertPdfToImagesFn = vi.fn();
|
||||
// Keep only mocks needed for the simplified handler
|
||||
const mockRunConversion = vi.fn(); // Mock the library function
|
||||
const mockExistsSync = vi.fn();
|
||||
const mockStatSync = vi.fn();
|
||||
const mockReadFile = vi.fn();
|
||||
const mockMkdir = vi.fn();
|
||||
const mockDirname = vi.fn();
|
||||
const mockBasename = vi.fn();
|
||||
const mockExtname = vi.fn();
|
||||
const mockResolve = vi.fn();
|
||||
const mockParse = vi.fn();
|
||||
const mockRelative = vi.fn();
|
||||
const mockLoggerInfo = vi.fn();
|
||||
const mockLoggerError = vi.fn();
|
||||
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
||||
|
||||
// Mocks for @polymech/commons
|
||||
const mockResolveVariables = vi.fn();
|
||||
const mockPathInfoEx = vi.fn();
|
||||
const mockDEFAULT_ROOTS = { CWD: '/test/cwd', SCRIPT_DIR: '/test/script' };
|
||||
const mockDEFAULT_VARS = vi.fn().mockReturnValue({ SOME_DEFAULT: 'value' });
|
||||
// Remove mocks for functions no longer directly called by handler
|
||||
// const mockConvertPdfToImagesFn = vi.fn();
|
||||
// const mockStatSync = vi.fn();
|
||||
// const mockReadFile = vi.fn();
|
||||
// const mockMkdir = vi.fn();
|
||||
// const mockDirname = vi.fn();
|
||||
// const mockBasename = vi.fn();
|
||||
// const mockExtname = vi.fn();
|
||||
// const mockResolve = vi.fn();
|
||||
// const mockParse = vi.fn();
|
||||
// const mockRelative = vi.fn();
|
||||
// const mockResolveVariables = vi.fn();
|
||||
// const mockPathInfoEx = vi.fn();
|
||||
// const mockDEFAULT_ROOTS = { CWD: '/test/cwd', SCRIPT_DIR: '/test/script' };
|
||||
// const mockDEFAULT_VARS = vi.fn().mockReturnValue({ SOME_DEFAULT: 'value' });
|
||||
|
||||
// Use beforeAll for mocks
|
||||
beforeAll(() => {
|
||||
// Mock dependencies using vi.doMock
|
||||
vi.doMock('../../src/lib/pdf.js', () => ({
|
||||
convertPdfToImages: mockConvertPdfToImagesFn,
|
||||
// Remove unused mocks
|
||||
// vi.doMock('../../src/lib/pdf.js', ...)
|
||||
vi.doMock('../../src/lib/convert.js', () => ({
|
||||
runConversion: mockRunConversion, // Mock the refactored library function
|
||||
}));
|
||||
vi.doMock('node:fs', () => ({
|
||||
existsSync: mockExistsSync,
|
||||
statSync: mockStatSync,
|
||||
}));
|
||||
vi.doMock('node:fs/promises', () => ({
|
||||
readFile: mockReadFile,
|
||||
mkdir: mockMkdir,
|
||||
}));
|
||||
vi.doMock('node:path', () => ({
|
||||
dirname: mockDirname,
|
||||
basename: mockBasename,
|
||||
extname: mockExtname,
|
||||
resolve: mockResolve,
|
||||
parse: mockParse,
|
||||
relative: mockRelative,
|
||||
sep: '/',
|
||||
// statSync: mockStatSync,
|
||||
}));
|
||||
// vi.doMock('node:fs/promises', ...)
|
||||
// vi.doMock('node:path', ...)
|
||||
vi.doMock('tslog', () => ({
|
||||
Logger: vi.fn().mockImplementation(() => ({
|
||||
info: mockLoggerInfo,
|
||||
error: mockLoggerError,
|
||||
})),
|
||||
}));
|
||||
// Mock @polymech/commons
|
||||
vi.doMock('@polymech/commons', () => ({
|
||||
resolveVariables: mockResolveVariables,
|
||||
pathInfoEx: mockPathInfoEx,
|
||||
DEFAULT_ROOTS: mockDEFAULT_ROOTS,
|
||||
DEFAULT_VARS: mockDEFAULT_VARS,
|
||||
}));
|
||||
// vi.doMock('@polymech/commons', ...)
|
||||
});
|
||||
|
||||
// --- Test Suite ---
|
||||
@ -78,7 +67,10 @@ describe('Convert Command CLI Handler', () => {
|
||||
});
|
||||
|
||||
// --- Helper Function to Run Handler ---
|
||||
// Helper remains largely the same
|
||||
async function runHandlerHelper(args: Partial<ConvertCommandConfig & { _: (string | number)[], $0: string, output?: string }>) {
|
||||
// Add default values for required fields if not provided in args,
|
||||
// reflecting what yargs + schema default would do.
|
||||
const fullArgs = {
|
||||
_: ['convert'],
|
||||
$0: 'test',
|
||||
@ -86,199 +78,95 @@ describe('Convert Command CLI Handler', () => {
|
||||
format: 'png',
|
||||
...args,
|
||||
} as Arguments<ConvertCommandConfig & {output?: string}>;
|
||||
// Make sure handler is loaded before calling
|
||||
|
||||
// Need to simulate the full argv object including potential var-* args
|
||||
// Zod schema parsing happens inside the handler now.
|
||||
|
||||
if (!convertHandler) throw new Error('Handler not loaded');
|
||||
await convertHandler(fullArgs);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset mocks
|
||||
mockConvertPdfToImagesFn.mockResolvedValue(['output/img_1.png']);
|
||||
// Reset only necessary mocks
|
||||
mockRunConversion.mockResolvedValue(['path/to/image1.png']); // Default success
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFile.mockResolvedValue(Buffer.from('fake-pdf-data'));
|
||||
mockMkdir.mockResolvedValue(undefined);
|
||||
|
||||
// Mock path functions more robustly
|
||||
mockDirname.mockImplementation((p) => {
|
||||
if (!p || p === '/') return '/';
|
||||
const lastSlash = p.lastIndexOf('/');
|
||||
if (lastSlash === -1) return '.'; // No slash, return current dir indicator
|
||||
if (lastSlash === 0) return '/'; // Root directory
|
||||
return p.substring(0, lastSlash);
|
||||
});
|
||||
mockBasename.mockImplementation((p) => p.substring(p.lastIndexOf('/') > 0 ? p.lastIndexOf('/') + 1 : 0));
|
||||
mockExtname.mockImplementation((p) => {
|
||||
const lastSlash = p.lastIndexOf('/');
|
||||
const dotIndex = p.lastIndexOf('.');
|
||||
return dotIndex > (lastSlash > -1 ? lastSlash : -1) ? p.substring(dotIndex) : '';
|
||||
});
|
||||
// Improved mockResolve to handle absolute/relative paths based on /test/cwd
|
||||
mockResolve.mockImplementation((...paths) => {
|
||||
let currentPath = '/test/cwd'; // Assume CWD
|
||||
for (const p of paths) {
|
||||
if (path.isAbsolute(p)) { // Use actual path.isAbsolute for check
|
||||
currentPath = p;
|
||||
} else {
|
||||
currentPath = path.join(currentPath, p); // Use actual path.join
|
||||
}
|
||||
}
|
||||
// Normalize (e.g., remove //, resolve ..)
|
||||
return path.normalize(currentPath).replace(/\\/g, '/');
|
||||
});
|
||||
mockParse.mockImplementation((p) => ({
|
||||
root: '/',
|
||||
dir: mockDirname(p),
|
||||
base: mockBasename(p),
|
||||
ext: mockExtname(p),
|
||||
name: mockBasename(p, mockExtname(p)),
|
||||
}));
|
||||
mockRelative.mockImplementation((from, to) => to.startsWith(from) ? to.substring(from.length + 1) : to);
|
||||
|
||||
// Reset @polymech/commons mocks
|
||||
mockResolveVariables.mockImplementation(async (template, _bool, vars) => template.replace(/\${(.*?)}/g, (_, key) => vars[key] ?? 'UNDEFINED'));
|
||||
mockPathInfoEx.mockImplementation((p) => ({
|
||||
ROOT: '/test/cwd',
|
||||
SRC_DIR: mockDirname(p),
|
||||
SRC_NAME: mockBasename(p, mockExtname(p)),
|
||||
SRC_EXT: mockExtname(p),
|
||||
}));
|
||||
mockStatSync.mockImplementation((p) => { throw new Error('File not found'); });
|
||||
|
||||
// Removed resets for unused mocks
|
||||
mockProcessExit.mockClear();
|
||||
});
|
||||
|
||||
// --- Test cases ---
|
||||
it('should call convertPdfToImages with correct default args when output is omitted', async () => {
|
||||
// --- Updated Test cases ---
|
||||
it('should call runConversion with config when output is omitted (uses default)', async () => {
|
||||
const args = {
|
||||
input: 'pdfs/document.pdf',
|
||||
// output is omitted
|
||||
};
|
||||
// Setup mocks for this case
|
||||
mockExistsSync.mockReturnValueOnce(true); // Explicitly mock for this input
|
||||
mockResolve.mockImplementation((p) => p.startsWith('/') ? p : `/test/cwd/${p}`);
|
||||
const expectedSrcDir = '/test/cwd/pdfs';
|
||||
const expectedSrcName = 'document';
|
||||
mockPathInfoEx.mockReturnValue({
|
||||
ROOT: '/test/cwd',
|
||||
SRC_DIR: expectedSrcDir,
|
||||
SRC_NAME: expectedSrcName,
|
||||
SRC_EXT: '.pdf'
|
||||
});
|
||||
|
||||
await runHandlerHelper(args);
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(args.input);
|
||||
expect(mockReadFile).toHaveBeenCalledWith(args.input);
|
||||
expect(mockMkdir).toHaveBeenCalledWith(expectedSrcDir, { recursive: true });
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledTimes(1);
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), {
|
||||
baseVariables: expect.objectContaining({
|
||||
SRC_DIR: expectedSrcDir,
|
||||
SRC_NAME: expectedSrcName,
|
||||
FORMAT: 'png',
|
||||
DPI: 300,
|
||||
SOME_DEFAULT: 'value',
|
||||
CWD: mockDEFAULT_ROOTS.CWD
|
||||
expect(mockRunConversion).toHaveBeenCalledTimes(1);
|
||||
// Check the config object passed to runConversion
|
||||
expect(mockRunConversion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: args.input,
|
||||
output: undefined, // Output should be undefined in the config passed from handler
|
||||
dpi: 300, // Default DPI
|
||||
format: 'png', // Default format
|
||||
// other args like startPage/endPage should be undefined
|
||||
}),
|
||||
outputPathTemplate: `${'${SRC_DIR}'}/${'${SRC_NAME}'}_${'${PAGE}'}.${'${FORMAT}'}`,
|
||||
dpi: 300,
|
||||
format: 'png',
|
||||
startPage: undefined,
|
||||
endPage: undefined,
|
||||
logger: expect.anything(),
|
||||
});
|
||||
expect.anything() // Logger instance
|
||||
);
|
||||
// Verify final success logs are called
|
||||
expect(mockLoggerInfo).toHaveBeenCalledWith('Conversion completed successfully');
|
||||
expect(mockLoggerInfo).toHaveBeenCalledWith(expect.stringContaining('Generated')); // Check for generated count message
|
||||
expect(mockProcessExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use custom output path template when provided', async () => {
|
||||
it('should call runConversion with custom output path template when provided', async () => {
|
||||
const customPattern = 'images/custom_${SRC_NAME}_page${PAGE}.${FORMAT}';
|
||||
const args = {
|
||||
input: 'in.pdf',
|
||||
output: 'images/custom_${SRC_NAME}_page${PAGE}.${FORMAT}',
|
||||
output: customPattern,
|
||||
};
|
||||
mockExistsSync.mockReturnValueOnce(true); // Explicitly mock for this input
|
||||
mockResolve.mockImplementation((p) => p.startsWith('/') ? p : `/test/cwd/${p}`);
|
||||
mockPathInfoEx.mockReturnValue({
|
||||
ROOT: '/test/cwd',
|
||||
SRC_DIR: '/test/cwd',
|
||||
SRC_NAME: 'in',
|
||||
SRC_EXT: '.pdf'
|
||||
});
|
||||
const expectedPatternDir = '/test/cwd/images';
|
||||
// Ensure dirname mock works for the expected resolved path
|
||||
// mockDirname.mockImplementation((p) => p === '/test/cwd/images/custom_in_pageUNDEFINED.png' ? expectedPatternDir : '/'); // Old complex mock removed, rely on general mock
|
||||
|
||||
|
||||
await runHandlerHelper(args);
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(expectedPatternDir, { recursive: true });
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({
|
||||
outputPathTemplate: args.output,
|
||||
baseVariables: expect.objectContaining({ SRC_NAME: 'in' }),
|
||||
}));
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(args.input);
|
||||
expect(mockRunConversion).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunConversion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: args.input,
|
||||
output: customPattern, // Expect the custom pattern string
|
||||
}),
|
||||
expect.anything() // Logger instance
|
||||
);
|
||||
expect(mockProcessExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle output path as a directory', async () => {
|
||||
it('should call runConversion with output path when it is a directory', async () => {
|
||||
const dirPath = 'output/images/';
|
||||
const args = {
|
||||
input: 'some/path/doc.pdf',
|
||||
output: 'output/images/',
|
||||
output: dirPath,
|
||||
};
|
||||
const resolvedOutputDir = '/test/cwd/output/images';
|
||||
mockResolve.mockImplementation((p) => p === args.output ? resolvedOutputDir : p );
|
||||
mockStatSync.mockImplementation((p) => {
|
||||
if (p === resolvedOutputDir) {
|
||||
return { isDirectory: () => true };
|
||||
}
|
||||
throw new Error('Not found');
|
||||
});
|
||||
mockPathInfoEx.mockReturnValue({
|
||||
ROOT: '/test/cwd',
|
||||
SRC_DIR: '/test/cwd/some/path',
|
||||
SRC_NAME: 'doc',
|
||||
SRC_EXT: '.pdf'
|
||||
});
|
||||
|
||||
await runHandlerHelper(args);
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(resolvedOutputDir, { recursive: true });
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({
|
||||
outputPathTemplate: `${'${OUT_DIR}'}/${'${SRC_NAME}'}_${'${PAGE}'}.${'${FORMAT}'}`,
|
||||
baseVariables: expect.objectContaining({
|
||||
OUT_DIR: resolvedOutputDir,
|
||||
SRC_NAME: 'doc',
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(args.input);
|
||||
expect(mockRunConversion).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunConversion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: args.input,
|
||||
output: dirPath, // Expect the directory path string
|
||||
}),
|
||||
}));
|
||||
expect.anything()
|
||||
);
|
||||
expect(mockProcessExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle output path that looks like a directory (ends with /) but doesnt exist yet', async () => {
|
||||
const args = {
|
||||
input: 'other.pdf',
|
||||
output: 'new_dir/',
|
||||
};
|
||||
const resolvedOutputDir = '/test/cwd/new_dir';
|
||||
mockResolve.mockImplementation((p) => p === args.output ? resolvedOutputDir : p );
|
||||
mockStatSync.mockImplementation((p) => { throw new Error('Not found'); });
|
||||
mockPathInfoEx.mockReturnValue({
|
||||
ROOT: '/test/cwd',
|
||||
SRC_DIR: '/test/cwd',
|
||||
SRC_NAME: 'other',
|
||||
SRC_EXT: '.pdf'
|
||||
});
|
||||
|
||||
await runHandlerHelper(args);
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(resolvedOutputDir, { recursive: true });
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({
|
||||
outputPathTemplate: `${'${OUT_DIR}'}/${'${SRC_NAME}'}_${'${PAGE}'}.${'${FORMAT}'}`,
|
||||
baseVariables: expect.objectContaining({
|
||||
OUT_DIR: resolvedOutputDir,
|
||||
SRC_NAME: 'other',
|
||||
}),
|
||||
}));
|
||||
expect(mockProcessExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call convertPdfToImages with specific args', async () => {
|
||||
// Test for specific args being passed through
|
||||
it('should call runConversion with specific args', async () => {
|
||||
const args = {
|
||||
input: 'input.pdf',
|
||||
output: 'output/prefix',
|
||||
@ -287,57 +175,80 @@ describe('Convert Command CLI Handler', () => {
|
||||
startPage: 2,
|
||||
endPage: 5,
|
||||
};
|
||||
mockResolve.mockImplementation((p) => p.startsWith('/') ? p : `/test/cwd/${p}`);
|
||||
const expectedPatternDir = '/test/cwd/output';
|
||||
mockDirname.mockImplementation((p) => p.startsWith(expectedPatternDir) ? expectedPatternDir : '/');
|
||||
mockPathInfoEx.mockReturnValue({ ROOT: '/test/cwd', SRC_DIR: '/test/cwd', SRC_NAME: 'input', SRC_EXT: '.pdf' });
|
||||
|
||||
|
||||
await runHandlerHelper(args);
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalledWith(args.input);
|
||||
expect(mockReadFile).toHaveBeenCalledWith(args.input);
|
||||
expect(mockMkdir).toHaveBeenCalledWith(expectedPatternDir, { recursive: true });
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledTimes(1);
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), {
|
||||
baseVariables: expect.objectContaining({
|
||||
SRC_NAME: 'input',
|
||||
FORMAT: 'jpg',
|
||||
DPI: 150
|
||||
expect(mockRunConversion).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunConversion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: args.input,
|
||||
output: args.output,
|
||||
dpi: args.dpi,
|
||||
format: args.format,
|
||||
startPage: args.startPage,
|
||||
endPage: args.endPage,
|
||||
}),
|
||||
outputPathTemplate: args.output,
|
||||
dpi: args.dpi,
|
||||
format: args.format,
|
||||
startPage: args.startPage,
|
||||
endPage: args.endPage,
|
||||
logger: expect.anything(),
|
||||
});
|
||||
expect.anything()
|
||||
);
|
||||
expect(mockProcessExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Test for var-* args being passed through
|
||||
it('should pass var-* arguments to runConversion', async () => {
|
||||
const args = {
|
||||
input: 'input.pdf',
|
||||
'var-MY_VAR': 'myValue',
|
||||
'var-OTHER': 123
|
||||
};
|
||||
|
||||
await runHandlerHelper(args);
|
||||
|
||||
expect(mockRunConversion).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunConversion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: args.input,
|
||||
'var-MY_VAR': 'myValue', // Zod schema with catchall should preserve these
|
||||
'var-OTHER': 123,
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
expect(mockProcessExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
// --- Error Handling Tests ---
|
||||
it('should handle missing input file', async () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
const args = { input: 'nonexistent.pdf', output: 'out' };
|
||||
const args = { input: 'nonexistent.pdf' }; // Output is optional
|
||||
await runHandlerHelper(args);
|
||||
expect(mockConvertPdfToImagesFn).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRunConversion).not.toHaveBeenCalled(); // Should not be called
|
||||
// Check logger error message (updated)
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error during conversion:'),
|
||||
"Error during conversion command:", // Updated error context
|
||||
expect.stringContaining('Input file nonexistent.pdf does not exist'),
|
||||
expect.any(Error)
|
||||
);
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle conversion error', async () => {
|
||||
const conversionError = new Error('Conversion failed');
|
||||
mockConvertPdfToImagesFn.mockRejectedValue(conversionError);
|
||||
it('should handle conversion error from runConversion', async () => {
|
||||
const conversionError = new Error('Conversion library failed');
|
||||
mockRunConversion.mockRejectedValue(conversionError); // Mock runConversion to throw
|
||||
const args = { input: 'in.pdf', output: 'out' };
|
||||
|
||||
await runHandlerHelper(args);
|
||||
expect(mockConvertPdfToImagesFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockRunConversion).toHaveBeenCalledTimes(1);
|
||||
// Check logger error message (updated)
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Error during conversion:',
|
||||
'Conversion failed',
|
||||
"Error during conversion command:", // Updated error context
|
||||
conversionError.message,
|
||||
conversionError
|
||||
);
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
// Remove tests checking internal logic that was moved (e.g., mkdir calls)
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user