export interface SelectedGameFile { path: string; file: File } export interface SelectedGameFolder { name: string; files: SelectedGameFile[]; size: number } /** Directory inputs include the selected folder in every relative path. Remove * that one component and retain the complete game directory below it. */ export function selectGameFolder(selection: Iterable): SelectedGameFolder { const files: SelectedGameFile[] = []; const paths = new Set(); let name = ''; let size = 0; for (const file of selection) { const [root, ...parts] = file.webkitRelativePath.split('/'); if (!root || root === '.' || root === '..' || !parts.length || name && root !== name) throw new Error('Choose one game folder with the folder picker.'); name = root; const path = parts.join('/'); if (path.length > 1024 || /[\\\x00-\x1f\x7f:]/.test(path) || parts.some(part => !part || part === '.' || part === '..' || /[. ]$/.test(part))) { throw new Error(`This file has an unsupported game path: ${file.webkitRelativePath}`); } const key = path.toLowerCase(); if (paths.has(key)) throw new Error(`This folder has duplicate game paths: ${path}`); paths.add(key); size += file.size; if (!Number.isSafeInteger(size)) throw new Error('This folder exceeds the supported upload size.'); files.push({ path, file }); if (files.length > 200_000) throw new Error('Choose a folder with no more than 200,000 files.'); } if (!files.length) throw new Error('The selected folder contains no files.'); for (const { path } of files) { const parents = path.toLowerCase().split('/'); parents.pop(); while (parents.length) { if (paths.has(parents.join('/'))) throw new Error(`This folder uses the same game path for a file and a directory: ${path}`); parents.pop(); } } files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); return { name, files, size }; }