securityos/components/system/Files/FileManager/functions.ts

306 lines
8.5 KiB
TypeScript

import type Stats from "browserfs/dist/node/core/node_fs_stats";
import type {
FileReaders,
ObjectReaders,
} from "components/system/Dialogs/Transfer/useTransferDialog";
import { getModifiedTime } from "components/system/Files/FileEntry/functions";
import type {
CompleteAction,
Files,
} from "components/system/Files/FileManager/useFolder";
import { COMPLETE_ACTION } from "components/system/Files/FileManager/useFolder";
import type { SortBy } from "components/system/Files/FileManager/useSortBy";
import { basename, dirname, extname, join } from "path";
import { ONE_TIME_PASSIVE_EVENT, ROOT_SHORTCUT } from "utils/constants";
import { haltEvent } from "utils/functions";
export type FileStat = Stats & {
systemShortcut?: boolean;
};
type FileStats = [string, FileStat];
type SortFunction = (a: FileStats, b: FileStats) => number;
export const sortByDate =
(directory: string) =>
([aPath, aStats]: FileStats, [bPath, bStats]: FileStats): number =>
getModifiedTime(join(directory, aPath), aStats) -
getModifiedTime(join(directory, bPath), bStats);
const sortByName = ([a]: FileStats, [b]: FileStats): number =>
a.localeCompare(b, "en", { sensitivity: "base" });
export const sortBySize = (
[, { size: aSize }]: FileStats,
[, { size: bSize }]: FileStats
): number => aSize - bSize;
const sortByType = ([a]: FileStats, [b]: FileStats): number =>
extname(a).localeCompare(extname(b), "en", { sensitivity: "base" });
const sortSystemShortcuts = (
[aName, { systemShortcut: aSystem = false }]: FileStats,
[bName, { systemShortcut: bSystem = false }]: FileStats
): number => {
if (aSystem === bSystem) {
if (bSystem && bName === ROOT_SHORTCUT) return 1;
return aSystem && aName === ROOT_SHORTCUT ? -1 : 0;
}
return aSystem ? -1 : 1;
};
export const sortContents = (
contents: Files,
sortOrder: string[],
sortFunction?: SortFunction,
ascending = true
): Files => {
if (sortOrder.length > 0) {
const contentOrder = Object.keys(contents);
return Object.fromEntries(
sortOrder
.filter((entry) => contentOrder.includes(entry))
// eslint-disable-next-line unicorn/prefer-spread
.concat(contentOrder.filter((entry) => !sortOrder.includes(entry)))
.map((entry) => [entry, contents[entry]])
);
}
const files: FileStats[] = [];
const folders: FileStats[] = [];
Object.entries(contents).forEach((entry) => {
const [, stat] = entry;
if (stat.isDirectory()) folders.push(entry);
else files.push(entry);
});
const sortContent = (fileStats: FileStats[]): FileStats[] => {
fileStats.sort(sortByName);
return sortFunction && sortFunction !== sortByName
? fileStats.sort(sortFunction)
: fileStats;
};
const sortedFolders = sortContent(folders);
const sortedFiles = sortContent(files);
if (!ascending) {
sortedFolders.reverse();
sortedFiles.reverse();
}
return Object.fromEntries(
(ascending
? [...sortedFolders, ...sortedFiles]
: [...sortedFiles, ...sortedFolders]
).sort(sortSystemShortcuts)
);
};
export const sortFiles = (
directory: string,
files: Files,
sortBy: SortBy,
ascending: boolean
): Files => {
const sortFunctionMap: Record<string, SortFunction> = {
date: sortByDate(directory),
name: sortByName,
size: sortBySize,
type: sortByType,
};
return sortBy in sortFunctionMap
? sortContents(files, [], sortFunctionMap[sortBy], ascending)
: files;
};
export const iterateFileName = (name: string, iteration: number): string => {
const extension = extname(name);
const fileName = basename(name, extension);
return `${fileName} (${iteration})${extension}`;
};
export const createFileReaders = async (
files: DataTransferItemList | FileList | never[],
directory: string,
callback: (
fileName: string,
buffer?: Buffer,
completeAction?: CompleteAction
) => void
): Promise<FileReaders> => {
const fileReaders: FileReaders = [];
const addFile = (file: File, subFolder = ""): void => {
const reader = new FileReader();
reader.addEventListener(
"load",
({ target }) => {
if (target?.result instanceof ArrayBuffer) {
callback(
join(subFolder, file.name),
Buffer.from(target.result),
files.length === 1 ? COMPLETE_ACTION.UPDATE_URL : undefined
);
}
},
ONE_TIME_PASSIVE_EVENT
);
fileReaders.push([file, join(directory, subFolder), reader]);
};
const addEntry = async (
fileSystemEntry: FileSystemEntry,
subFolder = ""
): Promise<void> =>
new Promise((resolve) => {
if (fileSystemEntry?.isDirectory) {
(fileSystemEntry as FileSystemDirectoryEntry)
.createReader()
.readEntries((entries) =>
Promise.all(
entries.map((entry) =>
addEntry(entry, join(subFolder, fileSystemEntry.name))
)
).then(() => resolve())
);
} else {
(fileSystemEntry as FileSystemFileEntry)?.file((file) => {
addFile(file, subFolder);
resolve();
});
}
});
if (files instanceof FileList) {
[...files].forEach((file) => addFile(file));
} else {
await Promise.all(
[...files].map(async (file) =>
addEntry(file.webkitGetAsEntry() as FileSystemEntry)
)
);
}
return fileReaders;
};
export type InputChangeEvent = Event & { target: HTMLInputElement };
type EventData = {
files: DataTransferItemList | FileList | never[];
text?: string;
};
export const getEventData = (
event: DragEvent | InputChangeEvent | never[] | React.DragEvent
): EventData => {
const dataTransfer =
(event as React.DragEvent).nativeEvent?.dataTransfer ||
(event as DragEvent).dataTransfer;
let files =
(event as InputChangeEvent).target?.files || dataTransfer?.items || [];
const text = dataTransfer?.getData("application/json");
if (files instanceof DataTransferItemList) {
files = [...files].filter(
(item) => !("kind" in item) || item.kind === "file"
) as unknown as DataTransferItemList;
}
return { files, text };
};
export const handleFileInputEvent = (
event: InputChangeEvent | React.DragEvent,
callback: (
fileName: string,
buffer?: Buffer,
completeAction?: CompleteAction
) => Promise<void>,
directory: string,
openTransferDialog: (fileReaders: FileReaders | ObjectReaders) => void,
hasUpdateId = false
): void => {
haltEvent(event);
const { files, text } = getEventData(event);
if (text) {
try {
const filePaths = JSON.parse(text) as string[];
if (!Array.isArray(filePaths)) return;
const isSingleFile = filePaths.length === 1;
const objectReaders = filePaths.map((filePath) => {
let aborted = false;
return {
abort: () => {
aborted = true;
},
directory,
name: filePath,
read: async () => {
if (aborted || dirname(filePath) === ".") return;
await callback(
filePath,
undefined,
isSingleFile ? COMPLETE_ACTION.UPDATE_URL : undefined
);
},
};
});
if (isSingleFile) {
const [singleFile] = objectReaders;
if (hasUpdateId) {
callback(singleFile.name, undefined, COMPLETE_ACTION.UPDATE_URL);
}
if (hasUpdateId || singleFile.directory === singleFile.name) return;
}
openTransferDialog(objectReaders);
} catch {
// Failed to parse text data to JSON
}
} else {
createFileReaders(files, directory, callback).then(openTransferDialog);
}
};
export const findPathsRecursive = async (
paths: string[],
readdir: (path: string) => Promise<string[]>,
lstat: (path: string) => Promise<Stats>
): Promise<string[]> => {
const pathArrays = await Promise.all(
paths.map(
async (path): Promise<string[]> =>
(await lstat(path)).isDirectory()
? findPathsRecursive(
(await readdir(path)).map((file) => join(path, file)),
readdir,
lstat
)
: [path]
)
);
return pathArrays.flat();
};
export const removeInvalidFilenameCharacters = (name = ""): string =>
name.replace(/["*/:<>?\\|]/g, "");