@knide/fs-prober
Version:
fs-prober is a browser-friendly NPM package for extracting file and folder structures from user-selected files or directories.
211 lines (186 loc) • 6.78 kB
text/typescript
import flatMap from "lodash.flatmap"
import map from "lodash.map"
import { nanoid } from "nanoid"
import {
getDirHandleEntries,
getHandleKind,
getRootHandle,
isDirEntry,
isFileEntry,
} from "./proberWebApi"
import type {
DataTransferDropEvent,
FileNode,
FolderNode,
HierarchyDetails,
HierarchyTree,
} from "./types"
export const probeHierarchy = async (
event: DataTransferDropEvent,
): Promise<HierarchyDetails | null> => {
if (!event.dataTransfer) throw new Error("Unable to generate hierarchyTree for drop event")
type RootHandle = NonNullable<ReturnType<typeof getRootHandle>>
const rootHandleDrafts: Array<RootHandle> = []
for (let i = 0; i < event.dataTransfer.items.length; i++) {
try {
const draftRootHandle = getRootHandle(event, i)
if (draftRootHandle === null || draftRootHandle === undefined) return null // Return because nothing is dropped yet
rootHandleDrafts.push(draftRootHandle)
} catch (error) {
console.error(`Error probing item ${i}:`, error)
}
}
const probeResults: HierarchyTree[] = []
for (let i = 0; i < rootHandleDrafts.length; i++) {
const rootHandle = await rootHandleDrafts[i]
if (!rootHandle) continue
const probeResult = await fsProber(rootHandle, false)
if (probeResult) probeResults.push(probeResult)
}
return mergeProbeResults(probeResults)
}
export const getPathByPathIds = (pathIds: string[], nameMap: Map<string, string>): string => {
let path = ""
for (const pathId of pathIds) {
const name = nameMap.get(pathId)
if (!name) throw new Error(`Name not found for pathId: ${pathId}`)
path = `${path}/${name}`
}
return path
}
export const getNameId = (name: string, nameMap: Map<string, string>): string => {
const nameId = nanoid()
nameMap.set(nameId, name)
return nameId
}
export function fsProber(
rootHandle: FileSystemEntry | FileSystemHandle,
autoMerge: false,
): Promise<HierarchyTree | null>
export function fsProber(
rootHandle: FileSystemEntry | FileSystemHandle,
autoMerge: false,
): Promise<HierarchyDetails | null>
export async function fsProber(rootHandle: FileSystemEntry | FileSystemHandle, autoMerge = true) {
if (!rootHandle) return null
const objectMap = new Map<string, FileNode | FolderNode>()
const nameMap = new Map<string, string>()
const hierarchyTree: HierarchyTree = {
emptyFolders: [],
allFolders: [],
allFiles: [],
rootHandle,
rootFolder: undefined,
rootFile: undefined,
nameMap,
objectMap,
}
const path = rootHandle.name
if (isDirEntry(rootHandle)) {
const lookupMaps = { objectMap, nameMap }
await traverseDirectory(rootHandle, path, hierarchyTree, lookupMaps)
hierarchyTree.rootFolder = hierarchyTree.allFolders[hierarchyTree.allFolders.length - 1]
// If the there is only one folder in `allFolders` and that too is empty, then it surely couldn't have executed hierarchyDetails.emptyFolders. So lets's check for that
const hasNotEnteredTraversals =
hierarchyTree.allFolders.length === 1 && hierarchyTree.allFolders[0]?.children.length === 0
if (hasNotEnteredTraversals && hierarchyTree.allFolders[0]) {
hierarchyTree.emptyFolders.push(hierarchyTree.allFolders[0])
}
} else if (isFileEntry(rootHandle)) {
const nameId = getNameId(rootHandle.name, nameMap)
const file: FileNode = {
name: rootHandle.name,
nameId,
pathIds: [nameId],
kind: "file",
isBranch: false,
path,
handle: rootHandle,
}
objectMap.set(nameId, file)
hierarchyTree.allFiles.push(file)
hierarchyTree.rootFile = file
}
hierarchyTree.nameMap = nameMap
hierarchyTree.objectMap = objectMap
if (!autoMerge) return hierarchyTree
else return mergeProbeResults([hierarchyTree])
}
const traverseDirectory = async (
dirHandle: FileSystemDirectoryEntry | FileSystemDirectoryHandle,
currentPath: string,
hierarchyTree: HierarchyTree,
lookupMaps: { objectMap: HierarchyTree["objectMap"]; nameMap: HierarchyTree["nameMap"] },
pathIds: string[] = [],
) => {
const handleKind = getHandleKind(dirHandle)
if (handleKind !== "directory") return
const { objectMap, nameMap } = lookupMaps
const folderNameId = getNameId(dirHandle.name, nameMap)
const folderDetails: FolderNode = {
name: dirHandle.name,
nameId: folderNameId,
pathIds: [...pathIds, folderNameId],
kind: handleKind, // always "directory"
isBranch: true,
path: currentPath,
children: [],
handle: dirHandle,
}
objectMap.set(folderNameId, folderDetails)
const dirEntries = await getDirHandleEntries(dirHandle)
for (const [name, handle] of dirEntries) {
const path = `${currentPath}/${name}`
const entryHandleKind = getHandleKind(handle)
if (entryHandleKind === "file" && isFileEntry(handle)) {
const nameId = getNameId(handle.name, nameMap)
const file: FileNode = {
path,
pathIds: [...pathIds, folderNameId, nameId],
name: handle.name,
nameId,
kind: entryHandleKind,
isBranch: false,
handle,
}
objectMap.set(nameId, file)
hierarchyTree.allFiles.push(file)
folderDetails.children.push(file)
} else if (isDirEntry(handle)) {
const cpids = [...pathIds, folderNameId] // childPathIds
const lkupMaps = { objectMap, nameMap }
const childDetails = //
await traverseDirectory(handle, path, hierarchyTree, lkupMaps, cpids)
if (childDetails) {
if (childDetails.children.length === 0) {
hierarchyTree.emptyFolders.push(childDetails)
}
folderDetails.children.push(childDetails)
}
}
}
hierarchyTree.allFolders.push(folderDetails)
return folderDetails
}
const mergeProbeResults = (probeResults: HierarchyTree[]) => {
if (!probeResults || probeResults.length === 0) return null
let mergedNameMapArray: [string, string][] = []
probeResults.forEach((probeResult) => {
mergedNameMapArray = [...mergedNameMapArray, ...probeResult.nameMap]
})
let mergedObjectMapArray: [string, FolderNode | FileNode][] = []
probeResults.forEach((probeResult) => {
mergedObjectMapArray = [...mergedObjectMapArray, ...probeResult.objectMap]
})
const merged: HierarchyDetails = {
emptyFolders: flatMap(probeResults, "emptyFolders"),
allFolders: flatMap(probeResults, "allFolders"),
allFiles: flatMap(probeResults, "allFiles"),
rootHandles: map(probeResults, "rootHandle"),
rootFolders: map(probeResults, "rootFolder").filter(Boolean) as FolderNode[],
rootFiles: map(probeResults, "rootFile").filter(Boolean) as FileNode[],
nameMap: new Map(mergedNameMapArray),
objectMap: new Map(mergedObjectMapArray),
}
return merged
}