@jsenv/filesystem
Advanced tools
Comparing version 2.4.0 to 2.5.0
{ | ||
"name": "@jsenv/filesystem", | ||
"version": "2.4.0", | ||
"version": "2.5.0", | ||
"description": "Collection of functions to interact with filesystem in Node.js", | ||
@@ -19,4 +19,3 @@ "license": "MIT", | ||
"publishConfig": { | ||
"access": "public", | ||
"registry": "https://registry.npmjs.org" | ||
"access": "public" | ||
}, | ||
@@ -51,3 +50,3 @@ "type": "module", | ||
"dependencies": { | ||
"@jsenv/cancellation": "3.0.0", | ||
"@jsenv/abort": "3.1.1", | ||
"@jsenv/url-meta": "6.0.1" | ||
@@ -54,0 +53,0 @@ }, |
@@ -1,2 +0,2 @@ | ||
import { createCancellationToken, createOperation } from "@jsenv/cancellation" | ||
import { Abort } from "@jsenv/abort" | ||
import { | ||
@@ -16,3 +16,3 @@ normalizeStructuredMetaMap, | ||
export const collectDirectoryMatchReport = async ({ | ||
cancellationToken = createCancellationToken(), | ||
signal = new AbortController().signal, | ||
directoryUrl, | ||
@@ -34,7 +34,8 @@ structuredMetaMap, | ||
const collectOperation = Abort.startOperation() | ||
collectOperation.addAbortSignal(signal) | ||
const visitDirectory = async (directoryUrl) => { | ||
const directoryItems = await createOperation({ | ||
cancellationToken, | ||
start: () => readDirectory(directoryUrl), | ||
}) | ||
collectOperation.throwIfAborted() | ||
const directoryItems = await readDirectory(directoryUrl) | ||
@@ -49,15 +50,15 @@ await Promise.all( | ||
const directoryChildNodeStats = await createOperation({ | ||
cancellationToken, | ||
start: () => | ||
readFileSystemNodeStat(directoryChildNodeUrl, { | ||
// we ignore symlink because recursively traversed | ||
// so symlinked file will be discovered. | ||
// Moreover if they lead outside of directoryPath it can become a problem | ||
// like infinite recursion of whatever. | ||
// that we could handle using an object of pathname already seen but it will be useless | ||
// because directoryPath is recursively traversed | ||
followLink: false, | ||
}), | ||
}) | ||
collectOperation.throwIfAborted() | ||
const directoryChildNodeStats = await readFileSystemNodeStat( | ||
directoryChildNodeUrl, | ||
{ | ||
// we ignore symlink because recursively traversed | ||
// so symlinked file will be discovered. | ||
// Moreover if they lead outside of directoryPath it can become a problem | ||
// like infinite recursion of whatever. | ||
// that we could handle using an object of pathname already seen but it will be useless | ||
// because directoryPath is recursively traversed | ||
followLink: false, | ||
}, | ||
) | ||
@@ -110,7 +111,12 @@ if (directoryChildNodeStats.isDirectory()) { | ||
} | ||
await visitDirectory(rootDirectoryUrl) | ||
return { | ||
matchingArray: sortByRelativeUrl(matchingArray), | ||
ignoredArray: sortByRelativeUrl(ignoredArray), | ||
try { | ||
await visitDirectory(rootDirectoryUrl) | ||
return { | ||
matchingArray: sortByRelativeUrl(matchingArray), | ||
ignoredArray: sortByRelativeUrl(ignoredArray), | ||
} | ||
} finally { | ||
await collectOperation.end() | ||
} | ||
@@ -117,0 +123,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { createCancellationToken, createOperation } from "@jsenv/cancellation" | ||
import { Abort } from "@jsenv/abort" | ||
import { | ||
@@ -15,7 +15,6 @@ normalizeStructuredMetaMap, | ||
export const collectFiles = async ({ | ||
cancellationToken = createCancellationToken(), | ||
signal = new AbortController().signal, | ||
directoryUrl, | ||
structuredMetaMap, | ||
predicate, | ||
matchingFileOperation = () => null, | ||
}) => { | ||
@@ -26,7 +25,2 @@ const rootDirectoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl) | ||
} | ||
if (typeof matchingFileOperation !== "function") { | ||
throw new TypeError( | ||
`matchingFileOperation must be a function, got ${matchingFileOperation}`, | ||
) | ||
} | ||
const structuredMetaMapNormalized = normalizeStructuredMetaMap( | ||
@@ -37,8 +31,9 @@ structuredMetaMap, | ||
const collectOperation = Abort.startOperation() | ||
collectOperation.addAbortSignal(signal) | ||
const matchingFileResultArray = [] | ||
const visitDirectory = async (directoryUrl) => { | ||
const directoryItems = await createOperation({ | ||
cancellationToken, | ||
start: () => readDirectory(directoryUrl), | ||
}) | ||
collectOperation.throwIfAborted() | ||
const directoryItems = await readDirectory(directoryUrl) | ||
@@ -49,15 +44,15 @@ await Promise.all( | ||
const directoryChildNodeStats = await createOperation({ | ||
cancellationToken, | ||
start: () => | ||
readFileSystemNodeStat(directoryChildNodeUrl, { | ||
// we ignore symlink because recursively traversed | ||
// so symlinked file will be discovered. | ||
// Moreover if they lead outside of directoryPath it can become a problem | ||
// like infinite recursion of whatever. | ||
// that we could handle using an object of pathname already seen but it will be useless | ||
// because directoryPath is recursively traversed | ||
followLink: false, | ||
}), | ||
}) | ||
collectOperation.throwIfAborted() | ||
const directoryChildNodeStats = await readFileSystemNodeStat( | ||
directoryChildNodeUrl, | ||
{ | ||
// we ignore symlink because recursively traversed | ||
// so symlinked file will be discovered. | ||
// Moreover if they lead outside of directoryPath it can become a problem | ||
// like infinite recursion of whatever. | ||
// that we could handle using an object of pathname already seen but it will be useless | ||
// because directoryPath is recursively traversed | ||
followLink: false, | ||
}, | ||
) | ||
@@ -92,12 +87,2 @@ if (directoryChildNodeStats.isDirectory()) { | ||
) | ||
const operationResult = await createOperation({ | ||
cancellationToken, | ||
start: () => | ||
matchingFileOperation({ | ||
cancellationToken, | ||
relativeUrl, | ||
meta, | ||
fileStats: directoryChildNodeStats, | ||
}), | ||
}) | ||
matchingFileResultArray.push({ | ||
@@ -108,3 +93,2 @@ url: new URL(relativeUrl, rootDirectoryUrl).href, | ||
fileStats: directoryChildNodeStats, | ||
operationResult, | ||
}) | ||
@@ -116,12 +100,17 @@ return | ||
} | ||
await visitDirectory(rootDirectoryUrl) | ||
// When we operate on thoose files later it feels more natural | ||
// to perform operation in the same order they appear in the filesystem. | ||
// It also allow to get a predictable return value. | ||
// For that reason we sort matchingFileResultArray | ||
matchingFileResultArray.sort((leftFile, rightFile) => { | ||
return comparePathnames(leftFile.relativeUrl, rightFile.relativeUrl) | ||
}) | ||
return matchingFileResultArray | ||
try { | ||
await visitDirectory(rootDirectoryUrl) | ||
// When we operate on thoose files later it feels more natural | ||
// to perform operation in the same order they appear in the filesystem. | ||
// It also allow to get a predictable return value. | ||
// For that reason we sort matchingFileResultArray | ||
matchingFileResultArray.sort((leftFile, rightFile) => { | ||
return comparePathnames(leftFile.relativeUrl, rightFile.relativeUrl) | ||
}) | ||
return matchingFileResultArray | ||
} finally { | ||
await collectOperation.end() | ||
} | ||
} |
@@ -0,1 +1,3 @@ | ||
import { Abort } from "@jsenv/abort" | ||
import { assertAndNormalizeDirectoryUrl } from "./assertAndNormalizeDirectoryUrl.js" | ||
@@ -8,3 +10,2 @@ import { readFileSystemNodeStat } from "./readFileSystemNodeStat.js" | ||
import { copyFileSystemNode } from "./copyFileSystemNode.js" | ||
import { urlTargetsSameFileSystemPath } from "./internal/urlTargetsSameFileSystemPath.js" | ||
@@ -14,2 +15,3 @@ import { statsToType } from "./internal/statsToType.js" | ||
export const copyDirectoryContent = async ({ | ||
signal = new AbortController().signal, | ||
from, | ||
@@ -70,15 +72,25 @@ to, | ||
const directoryEntries = await readDirectory(fromUrl) | ||
await Promise.all( | ||
directoryEntries.map(async (directoryEntry) => { | ||
const from = resolveUrl(directoryEntry, fromUrl) | ||
const to = resolveUrl(directoryEntry, toUrl) | ||
await copyFileSystemNode({ | ||
from, | ||
to, | ||
overwrite, | ||
followLink, | ||
}) | ||
}), | ||
) | ||
const copyOperation = Abort.startOperation() | ||
copyOperation.addAbortSignal(signal) | ||
try { | ||
copyOperation.throwIfAborted() | ||
const directoryEntries = await readDirectory(fromUrl) | ||
await Promise.all( | ||
directoryEntries.map(async (directoryEntry) => { | ||
const from = resolveUrl(directoryEntry, fromUrl) | ||
const to = resolveUrl(directoryEntry, toUrl) | ||
await copyOperation.withSignal(async (signal) => { | ||
await copyFileSystemNode({ | ||
signal, | ||
from, | ||
to, | ||
overwrite, | ||
followLink, | ||
}) | ||
}) | ||
}), | ||
) | ||
} finally { | ||
await copyOperation.end() | ||
} | ||
} |
@@ -1,3 +0,3 @@ | ||
/* eslint-disable import/max-dependencies */ | ||
import { copyFile as copyFileNode } from "node:fs" | ||
import { Abort } from "@jsenv/abort" | ||
@@ -24,2 +24,3 @@ import { urlTargetsSameFileSystemPath } from "./internal/urlTargetsSameFileSystemPath.js" | ||
export const copyFileSystemNode = async ({ | ||
signal = new AbortController().signal, | ||
from, | ||
@@ -72,23 +73,2 @@ to, | ||
if (destinationStats) { | ||
const sourceType = statsToType(sourceStats) | ||
const destinationType = statsToType(destinationStats) | ||
if (sourceType !== destinationType) { | ||
throw new Error( | ||
`cannot copy ${sourceType} from ${fromPath} to ${toPath} because destination exists and is not a ${sourceType} (it's a ${destinationType})`, | ||
) | ||
} | ||
if (!overwrite) { | ||
throw new Error( | ||
`cannot copy ${sourceType} from ${fromPath} to ${toPath} because destination exists and overwrite option is disabled`, | ||
) | ||
} | ||
// remove file, link, directory... | ||
await removeFileSystemNode(toUrl, { recursive: true, allowUseless: true }) | ||
} else { | ||
await ensureParentDirectories(toUrl) | ||
} | ||
if (sourceStats.isDirectory()) { | ||
@@ -98,3 +78,7 @@ toUrl = ensureUrlTrailingSlash(toUrl) | ||
const copyOperation = Abort.startOperation() | ||
copyOperation.addAbortSignal(signal) | ||
const visit = async (url, stats) => { | ||
copyOperation.throwIfAborted() | ||
if (stats.isFile() || stats.isCharacterDevice() || stats.isBlockDevice()) { | ||
@@ -203,3 +187,33 @@ await visitFile(url, stats) | ||
await visit(fromUrl, sourceStats) | ||
try { | ||
if (destinationStats) { | ||
const sourceType = statsToType(sourceStats) | ||
const destinationType = statsToType(destinationStats) | ||
if (sourceType !== destinationType) { | ||
throw new Error( | ||
`cannot copy ${sourceType} from ${fromPath} to ${toPath} because destination exists and is not a ${sourceType} (it's a ${destinationType})`, | ||
) | ||
} | ||
if (!overwrite) { | ||
throw new Error( | ||
`cannot copy ${sourceType} from ${fromPath} to ${toPath} because destination exists and overwrite option is disabled`, | ||
) | ||
} | ||
// remove file, link, directory... | ||
await removeFileSystemNode(toUrl, { | ||
signal: copyOperation.signal, | ||
recursive: true, | ||
allowUseless: true, | ||
}) | ||
} else { | ||
await ensureParentDirectories(toUrl) | ||
} | ||
copyOperation.throwIfAborted() | ||
await visit(fromUrl, sourceStats) | ||
} finally { | ||
await copyOperation.end() | ||
} | ||
} | ||
@@ -206,0 +220,0 @@ |
@@ -6,2 +6,3 @@ import { collectFiles } from "./collectFiles.js" | ||
directoryUrl = process.cwd(), | ||
signal, | ||
) => { | ||
@@ -27,2 +28,3 @@ if (typeof patterns === "string") { | ||
const fileDatas = await collectFiles({ | ||
signal, | ||
directoryUrl, | ||
@@ -29,0 +31,0 @@ structuredMetaMap: { |
@@ -0,1 +1,3 @@ | ||
import { Abort } from "@jsenv/abort" | ||
import { assertAndNormalizeDirectoryUrl } from "./assertAndNormalizeDirectoryUrl.js" | ||
@@ -8,3 +10,2 @@ import { readFileSystemNodeStat } from "./readFileSystemNodeStat.js" | ||
import { moveFileSystemNode } from "./moveFileSystemNode.js" | ||
import { urlTargetsSameFileSystemPath } from "./internal/urlTargetsSameFileSystemPath.js" | ||
@@ -14,2 +15,3 @@ import { statsToType } from "./internal/statsToType.js" | ||
export const moveDirectoryContent = async ({ | ||
signal = new AbortController().signal, | ||
from, | ||
@@ -70,15 +72,26 @@ to, | ||
const directoryEntries = await readDirectory(fromUrl) | ||
await Promise.all( | ||
directoryEntries.map(async (directoryEntry) => { | ||
const from = resolveUrl(directoryEntry, fromUrl) | ||
const to = resolveUrl(directoryEntry, toUrl) | ||
await moveFileSystemNode({ | ||
from, | ||
to, | ||
overwrite, | ||
followLink, | ||
}) | ||
}), | ||
) | ||
const moveOperation = Abort.startOperation() | ||
moveOperation.addAbortSignal(signal) | ||
try { | ||
moveOperation.throwIfAborted() | ||
const directoryEntries = await readDirectory(fromUrl) | ||
await Promise.all( | ||
directoryEntries.map(async (directoryEntry) => { | ||
const from = resolveUrl(directoryEntry, fromUrl) | ||
const to = resolveUrl(directoryEntry, toUrl) | ||
await moveOperation.withSignal(async (signal) => { | ||
await moveFileSystemNode({ | ||
signal, | ||
from, | ||
to, | ||
overwrite, | ||
followLink, | ||
}) | ||
}) | ||
}), | ||
) | ||
} finally { | ||
await moveOperation.end() | ||
} | ||
} |
@@ -1,3 +0,3 @@ | ||
/* eslint-disable import/max-dependencies */ | ||
import { rename } from "node:fs" | ||
import { Abort } from "@jsenv/abort" | ||
@@ -16,2 +16,3 @@ import { urlTargetsSameFileSystemPath } from "./internal/urlTargetsSameFileSystemPath.js" | ||
export const moveFileSystemNode = async ({ | ||
signal = new AbortController().signal, | ||
from, | ||
@@ -61,33 +62,47 @@ to, | ||
if (destinationStats) { | ||
const sourceType = statsToType(sourceStats) | ||
const destinationType = statsToType(destinationStats) | ||
const moveOperation = Abort.startOperation() | ||
moveOperation.addAbortSignal(signal) | ||
if (sourceType !== destinationType) { | ||
throw new Error( | ||
`cannot move ${sourceType} from ${fromPath} to ${toPath} because destination exists and is not a ${sourceType} (it's a ${destinationType})`, | ||
) | ||
try { | ||
if (destinationStats) { | ||
const sourceType = statsToType(sourceStats) | ||
const destinationType = statsToType(destinationStats) | ||
if (sourceType !== destinationType) { | ||
throw new Error( | ||
`cannot move ${sourceType} from ${fromPath} to ${toPath} because destination exists and is not a ${sourceType} (it's a ${destinationType})`, | ||
) | ||
} | ||
if (!overwrite) { | ||
throw new Error( | ||
`cannot move ${sourceType} from ${fromPath} to ${toPath} because destination exists and overwrite option is disabled`, | ||
) | ||
} | ||
// remove file, link, directory... | ||
await removeFileSystemNode(toUrl, { | ||
signal: moveOperation.signal, | ||
recursive: true, | ||
}) | ||
} else { | ||
await ensureParentDirectories(toUrl) | ||
} | ||
if (!overwrite) { | ||
throw new Error( | ||
`cannot move ${sourceType} from ${fromPath} to ${toPath} because destination exists and overwrite option is disabled`, | ||
) | ||
} | ||
// remove file, link, directory... | ||
await removeFileSystemNode(toUrl, { recursive: true }) | ||
} else { | ||
await ensureParentDirectories(toUrl) | ||
moveOperation.throwIfAborted() | ||
await moveNaive(fromPath, toPath, { | ||
handleCrossDeviceError: async () => { | ||
await copyFileSystemNode({ | ||
from: fromUrl, | ||
to: toUrl, | ||
preserveStat: true, | ||
}) | ||
await removeFileSystemNode(fromUrl, { | ||
signal: moveOperation.signal, | ||
recursive: true, | ||
}) | ||
}, | ||
}) | ||
} finally { | ||
await moveOperation.end() | ||
} | ||
await moveNaive(fromPath, toPath, { | ||
handleCrossDeviceError: async () => { | ||
await copyFileSystemNode({ | ||
from: fromUrl, | ||
to: toUrl, | ||
preserveStat: true, | ||
}) | ||
await removeFileSystemNode(fromUrl, { recursive: true }) | ||
}, | ||
}) | ||
} | ||
@@ -94,0 +109,0 @@ |
import { unlink, rmdir, openSync, closeSync } from "node:fs" | ||
import { Abort } from "@jsenv/abort" | ||
@@ -13,2 +14,3 @@ import { ensureUrlTrailingSlash } from "./internal/ensureUrlTrailingSlash.js" | ||
{ | ||
signal = new AbortController().signal, | ||
allowUseless = false, | ||
@@ -23,36 +25,45 @@ recursive = false, | ||
const sourceStats = await readFileSystemNodeStat(sourceUrl, { | ||
nullIfNotFound: true, | ||
followLink: false, | ||
}) | ||
if (!sourceStats) { | ||
if (allowUseless) { | ||
return | ||
const removeOperation = Abort.startOperation() | ||
removeOperation.addAbortSignal(signal) | ||
try { | ||
removeOperation.throwIfAborted() | ||
const sourceStats = await readFileSystemNodeStat(sourceUrl, { | ||
nullIfNotFound: true, | ||
followLink: false, | ||
}) | ||
if (!sourceStats) { | ||
if (allowUseless) { | ||
return | ||
} | ||
throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`) | ||
} | ||
throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`) | ||
} | ||
// https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats | ||
// FIFO and socket are ignored, not sure what they are exactly and what to do with them | ||
// other libraries ignore them, let's do the same. | ||
if ( | ||
sourceStats.isFile() || | ||
sourceStats.isSymbolicLink() || | ||
sourceStats.isCharacterDevice() || | ||
sourceStats.isBlockDevice() | ||
) { | ||
await removeNonDirectory( | ||
sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl, | ||
{ | ||
// https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats | ||
// FIFO and socket are ignored, not sure what they are exactly and what to do with them | ||
// other libraries ignore them, let's do the same. | ||
if ( | ||
sourceStats.isFile() || | ||
sourceStats.isSymbolicLink() || | ||
sourceStats.isCharacterDevice() || | ||
sourceStats.isBlockDevice() | ||
) { | ||
await removeNonDirectory( | ||
sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl, | ||
{ | ||
maxRetries, | ||
retryDelay, | ||
}, | ||
) | ||
} else if (sourceStats.isDirectory()) { | ||
await removeDirectory(ensureUrlTrailingSlash(sourceUrl), { | ||
signal: removeOperation.signal, | ||
recursive, | ||
maxRetries, | ||
retryDelay, | ||
}, | ||
) | ||
} else if (sourceStats.isDirectory()) { | ||
await removeDirectory(ensureUrlTrailingSlash(sourceUrl), { | ||
recursive, | ||
maxRetries, | ||
retryDelay, | ||
onlyContent, | ||
}) | ||
onlyContent, | ||
}) | ||
} | ||
} finally { | ||
await removeOperation.end() | ||
} | ||
@@ -110,5 +121,9 @@ } | ||
rootDirectoryUrl, | ||
{ maxRetries, retryDelay, recursive, onlyContent }, | ||
{ signal, maxRetries, retryDelay, recursive, onlyContent }, | ||
) => { | ||
const removeDirectoryOperation = Abort.startOperation() | ||
removeDirectoryOperation.addAbortSignal(signal) | ||
const visit = async (sourceUrl) => { | ||
removeDirectoryOperation.throwIfAborted() | ||
const sourceStats = await readFileSystemNodeStat(sourceUrl, { | ||
@@ -147,2 +162,3 @@ nullIfNotFound: true, | ||
: {} | ||
removeDirectoryOperation.throwIfAborted() | ||
await removeDirectoryNaive(directoryPath, { | ||
@@ -186,2 +202,3 @@ ...optionsFromRecursive, | ||
const removeDirectoryContent = async (directoryUrl) => { | ||
removeDirectoryOperation.throwIfAborted() | ||
const names = await readDirectory(directoryUrl) | ||
@@ -204,6 +221,10 @@ await Promise.all( | ||
if (onlyContent) { | ||
await removeDirectoryContent(rootDirectoryUrl) | ||
} else { | ||
await visitDirectory(rootDirectoryUrl) | ||
try { | ||
if (onlyContent) { | ||
await removeDirectoryContent(rootDirectoryUrl) | ||
} else { | ||
await visitDirectory(rootDirectoryUrl) | ||
} | ||
} finally { | ||
await removeDirectoryOperation.end() | ||
} | ||
@@ -210,0 +231,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
349081
5045
+ Added@jsenv/abort@3.1.1
+ Added@jsenv/abort@3.1.1(transitive)
- Removed@jsenv/cancellation@3.0.0
- Removed@jsenv/cancellation@3.0.0(transitive)