Template
feat: redesign directory upload workflow
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
export interface ScannedFile {
|
||||
file: File;
|
||||
relativePath: string;
|
||||
}
|
||||
|
||||
export type ScanResult =
|
||||
| { success: true; files: ScannedFile[] }
|
||||
| { success: false; error: string };
|
||||
|
||||
export function normalizePath(rawPath: string): string | null {
|
||||
if (!rawPath) return null;
|
||||
const normalized = rawPath.replace(/\\/g, '/');
|
||||
if (normalized.startsWith('/')) return null;
|
||||
if (/^[a-zA-Z]:\//.test(normalized)) return null;
|
||||
|
||||
const parts = normalized.split('/');
|
||||
if (parts.includes('') || parts.includes('.') || parts.includes('..')) return null;
|
||||
|
||||
if (parts.length < 2) return null; // Needs at least top dir and file name
|
||||
|
||||
parts.shift(); // Remove EXACTLY one top-level directory
|
||||
const finalPath = parts.join('/');
|
||||
|
||||
if (!finalPath) return null;
|
||||
return finalPath;
|
||||
}
|
||||
|
||||
function readEntriesPromise(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.readEntries(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function readAllEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
|
||||
const entries: FileSystemEntry[] = [];
|
||||
while (true) {
|
||||
const batch = await readEntriesPromise(reader);
|
||||
if (batch.length === 0) break;
|
||||
entries.push(...batch);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function getFilePromise(fileEntry: FileSystemFileEntry): Promise<File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileEntry.file(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function scanDirectory(dirEntry: FileSystemDirectoryEntry, pathPrefix: string, scannedFiles: ScannedFile[]): Promise<void> {
|
||||
const reader = dirEntry.createReader();
|
||||
const entries = await readAllEntries(reader);
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory) {
|
||||
await scanDirectory(entry as FileSystemDirectoryEntry, `${pathPrefix}${entry.name}/`, scannedFiles);
|
||||
} else if (entry.isFile) {
|
||||
const fileEntry = entry as FileSystemFileEntry;
|
||||
const file = await getFilePromise(fileEntry);
|
||||
const relativePath = `${pathPrefix}${entry.name}`;
|
||||
scannedFiles.push({ file, relativePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function scanDataTransfer(dataTransfer: DataTransfer): Promise<ScanResult> {
|
||||
const items = dataTransfer.items;
|
||||
if (!items || items.length === 0) {
|
||||
return { success: false, error: '未检测到文件或目录' };
|
||||
}
|
||||
|
||||
const entries: FileSystemEntry[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.webkitGetAsEntry) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return { success: false, error: '无法读取拖拽内容' };
|
||||
}
|
||||
|
||||
// Reject multiple top-level directories or loose files
|
||||
if (entries.length > 1) {
|
||||
return { success: false, error: '请每次仅拖入或选择一个目录' };
|
||||
}
|
||||
|
||||
const rootEntry = entries[0];
|
||||
if (!rootEntry.isDirectory) {
|
||||
return { success: false, error: '请拖入或选择一个目录,而不是单个文件' };
|
||||
}
|
||||
|
||||
try {
|
||||
const scannedFiles: ScannedFile[] = [];
|
||||
// Start scanning with the top-level directory name in the path prefix
|
||||
await scanDirectory(rootEntry as FileSystemDirectoryEntry, `${rootEntry.name}/`, scannedFiles);
|
||||
|
||||
// Filter out invalid paths if any
|
||||
const validFiles: ScannedFile[] = [];
|
||||
for (const item of scannedFiles) {
|
||||
const norm = normalizePath(item.relativePath);
|
||||
if (norm) {
|
||||
validFiles.push({ file: item.file, relativePath: norm });
|
||||
} else {
|
||||
return { success: false, error: '目录包含无效的路径格式' };
|
||||
}
|
||||
}
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
return { success: false, error: '选中的目录为空或没有有效文件' };
|
||||
}
|
||||
|
||||
return { success: true, files: validFiles };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, error: '扫描目录时发生错误' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user