Rearchitect import (#6141)
This commit is contained in:
67
server/utils/ImportHelper.ts
Normal file
67
server/utils/ImportHelper.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import { deserializeFilename } from "./fs";
|
||||
|
||||
export type FileTreeNode = {
|
||||
/** The title, extracted from the file name */
|
||||
title: string;
|
||||
/** The file name including extension */
|
||||
name: string;
|
||||
/** Full path to the file within the zip file */
|
||||
path: string;
|
||||
/** Any nested children */
|
||||
children: FileTreeNode[];
|
||||
};
|
||||
|
||||
export default class ImportHelper {
|
||||
/**
|
||||
* Collects the files and folders for a directory filePath.
|
||||
*/
|
||||
public static async toFileTree(
|
||||
filePath: string,
|
||||
currentDepth = 0
|
||||
): Promise<FileTreeNode | null> {
|
||||
const name = path.basename(filePath);
|
||||
const title = deserializeFilename(path.parse(path.basename(name)).name);
|
||||
const item = {
|
||||
path: filePath,
|
||||
name,
|
||||
title,
|
||||
children: [] as FileTreeNode[],
|
||||
};
|
||||
let stats;
|
||||
|
||||
if ([".DS_Store", "__MACOSX"].includes(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
stats = await fs.stat(filePath);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stats.isFile()) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const dirData = await fs.readdir(filePath);
|
||||
if (dirData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
item.children = (
|
||||
await Promise.all(
|
||||
dirData.map((child) =>
|
||||
this.toFileTree(path.join(filePath, child), currentDepth + 1)
|
||||
)
|
||||
)
|
||||
).filter(Boolean) as FileTreeNode[];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,9 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import JSZip from "jszip";
|
||||
import find from "lodash/find";
|
||||
import tmp from "tmp";
|
||||
import { bytesToHumanReadable } from "@shared/utils/files";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { trace } from "@server/logging/tracing";
|
||||
import { deserializeFilename } from "./fs";
|
||||
|
||||
export type FileTreeNode = {
|
||||
/** The title, extracted from the file name */
|
||||
title: string;
|
||||
/** The file name including extension */
|
||||
name: string;
|
||||
/** Full path to the file within the zip file */
|
||||
path: string;
|
||||
/** Any nested children */
|
||||
children: FileTreeNode[];
|
||||
};
|
||||
|
||||
@trace()
|
||||
export default class ZipHelper {
|
||||
@@ -32,62 +17,6 @@ export default class ZipHelper {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the flat structure returned by JSZIP into a nested file structure
|
||||
* for easier processing.
|
||||
*
|
||||
* @param zip The JSZip instance
|
||||
* @param maxFiles The maximum number of files to unzip (Prevent zip bombs)
|
||||
*/
|
||||
public static toFileTree(
|
||||
zip: JSZip,
|
||||
/** The maximum number of files to unzip */
|
||||
maxFiles = 10000
|
||||
) {
|
||||
const paths = ZipHelper.getPathsInZip(zip, maxFiles);
|
||||
const tree: FileTreeNode[] = [];
|
||||
|
||||
paths.forEach(function (filePath) {
|
||||
if (filePath.startsWith("/__MACOSX")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathParts = filePath.split("/");
|
||||
|
||||
// Remove first blank element from the parts array.
|
||||
pathParts.shift();
|
||||
|
||||
let currentLevel = tree; // initialize currentLevel to root
|
||||
|
||||
pathParts.forEach(function (name) {
|
||||
// check to see if the path already exists.
|
||||
const existingPath = find(currentLevel, {
|
||||
name,
|
||||
});
|
||||
|
||||
if (existingPath) {
|
||||
// The path to this item was already in the tree, so don't add again.
|
||||
// Set the current level to this path's children
|
||||
currentLevel = existingPath.children;
|
||||
} else if (name.endsWith(".DS_Store") || !name) {
|
||||
return;
|
||||
} else {
|
||||
const newPart = {
|
||||
name,
|
||||
path: filePath.replace(/^\//, ""),
|
||||
title: deserializeFilename(path.parse(path.basename(name)).name),
|
||||
children: [],
|
||||
};
|
||||
|
||||
currentLevel.push(newPart);
|
||||
currentLevel = newPart.children;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a zip file to a temporary disk location
|
||||
*
|
||||
@@ -158,34 +87,4 @@ export default class ZipHelper {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of file paths contained within the ZIP file, accounting for
|
||||
* differences between OS.
|
||||
*
|
||||
* @param zip The JSZip instance
|
||||
* @param maxFiles The maximum number of files to unzip (Prevent zip bombs)
|
||||
*/
|
||||
private static getPathsInZip(zip: JSZip, maxFiles = 10000) {
|
||||
let fileCount = 0;
|
||||
const paths: string[] = [];
|
||||
|
||||
Object.keys(zip.files).forEach((p) => {
|
||||
if (++fileCount > maxFiles) {
|
||||
throw ValidationError("Too many files in zip");
|
||||
}
|
||||
|
||||
const filePath = `/${p}`;
|
||||
|
||||
// "zip.files" for ZIPs created on Windows does not return paths for
|
||||
// directories, so we must add them manually if missing.
|
||||
const dir = filePath.slice(0, filePath.lastIndexOf("/") + 1);
|
||||
if (dir.length > 1 && !paths.includes(dir)) {
|
||||
paths.push(dir);
|
||||
}
|
||||
|
||||
paths.push(filePath);
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user