mirror of
https://github.com/yamadashy/repomix.git
synced 2026-02-03 11:33:39 +01:00
- Remove unused imports across 67 files (RepomixConfigMerged, QueryCapture, etc.) - Fix unused parameters by prefixing with underscore (_context, _index, etc.) - Remove unused catch parameters using modern JavaScript syntax - Fix require-yield warnings in generator functions - Remove unused variables and interface declarations - Add oxlint configuration to ignore integration test fixtures Resolves 144 linting warnings while preserving all functionality. All 743 tests continue to pass. Code quality significantly improved.
359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
import { computed, ref } from 'vue';
|
|
|
|
export interface FileUploadOptions {
|
|
maxFileSize?: number;
|
|
acceptedTypes?: string[];
|
|
accept?: string;
|
|
multiple?: boolean;
|
|
webkitdirectory?: boolean;
|
|
validateFile?: (file: File) => { valid: boolean; error?: string };
|
|
validateFiles?: (files: File[]) => { valid: boolean; error?: string };
|
|
preprocessFiles?: (files: File[], folderName?: string) => Promise<File>;
|
|
}
|
|
|
|
export interface FileUploadConfig {
|
|
mode: 'file' | 'folder';
|
|
placeholder: string;
|
|
icon: 'file' | 'folder';
|
|
options: FileUploadOptions;
|
|
}
|
|
|
|
export function useFileUpload(config: FileUploadConfig) {
|
|
const {
|
|
maxFileSize = 10 * 1024 * 1024, // 10MB default
|
|
acceptedTypes = [],
|
|
accept = '',
|
|
multiple = false,
|
|
webkitdirectory = false,
|
|
validateFile,
|
|
validateFiles,
|
|
preprocessFiles,
|
|
} = config.options;
|
|
|
|
// Reactive state
|
|
const fileInput = ref<HTMLInputElement | null>(null);
|
|
const dragActive = ref(false);
|
|
const selectedItem = ref<string | null>(null);
|
|
const errorMessage = ref<string | null>(null);
|
|
const isProcessing = ref(false);
|
|
|
|
// Computed
|
|
const hasError = computed(() => !!errorMessage.value);
|
|
const hasSelection = computed(() => !!selectedItem.value);
|
|
const isValid = computed(() => hasSelection.value && !hasError.value);
|
|
|
|
// Default file validation
|
|
function defaultValidateFile(file: File): { valid: boolean; error?: string } {
|
|
if (acceptedTypes.length > 0) {
|
|
const isValidType = acceptedTypes.some((type) => {
|
|
if (type.startsWith('.')) {
|
|
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
}
|
|
return file.type === type;
|
|
});
|
|
|
|
if (!isValidType) {
|
|
return {
|
|
valid: false,
|
|
error: `Please upload a ${acceptedTypes.join(' or ')} file`,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (file.size > maxFileSize) {
|
|
const sizeMB = (file.size / (1024 * 1024)).toFixed(1);
|
|
const limitMB = (maxFileSize / (1024 * 1024)).toFixed(0);
|
|
return {
|
|
valid: false,
|
|
error: `File size (${sizeMB}MB) exceeds the ${limitMB}MB limit`,
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
// Default files validation (for folder/multiple files)
|
|
function defaultValidateFiles(files: File[]): { valid: boolean; error?: string } {
|
|
if (files.length === 0) {
|
|
return { valid: false, error: 'No files found' };
|
|
}
|
|
|
|
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
if (totalSize > maxFileSize) {
|
|
const sizeMB = (totalSize / (1024 * 1024)).toFixed(1);
|
|
const limitMB = (maxFileSize / (1024 * 1024)).toFixed(0);
|
|
return {
|
|
valid: false,
|
|
error: `Total size (${sizeMB}MB) exceeds the ${limitMB}MB limit`,
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
// Clear error and selection
|
|
function clearError() {
|
|
errorMessage.value = null;
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedItem.value = null;
|
|
errorMessage.value = null;
|
|
// Clear file input to prevent re-selection issues
|
|
if (fileInput.value) {
|
|
fileInput.value.value = '';
|
|
}
|
|
}
|
|
|
|
// Validate and process files
|
|
async function processFiles(
|
|
files: File[],
|
|
folderName?: string,
|
|
): Promise<{ success: boolean; result?: File; error?: string }> {
|
|
clearError();
|
|
isProcessing.value = true;
|
|
|
|
try {
|
|
// Validation
|
|
const validator =
|
|
config.mode === 'folder' || multiple
|
|
? validateFiles || defaultValidateFiles
|
|
: validateFile || defaultValidateFile;
|
|
|
|
let validationResult: { valid: boolean; error?: string };
|
|
if (config.mode === 'folder' || multiple) {
|
|
validationResult = (validator as typeof defaultValidateFiles)(files);
|
|
} else {
|
|
validationResult = (validator as typeof defaultValidateFile)(files[0]);
|
|
}
|
|
|
|
if (!validationResult.valid) {
|
|
errorMessage.value = validationResult.error || 'Validation failed';
|
|
return { success: false, error: validationResult.error };
|
|
}
|
|
|
|
// Preprocessing (e.g., ZIP creation for folders)
|
|
let resultFile: File;
|
|
if (preprocessFiles) {
|
|
resultFile = await preprocessFiles(files, folderName);
|
|
} else {
|
|
if ((config.mode === 'folder' || multiple) && files.length > 1) {
|
|
throw new Error('Multiple files require a preprocessor function');
|
|
}
|
|
resultFile = files[0];
|
|
}
|
|
|
|
// Update selection
|
|
selectedItem.value = folderName || resultFile.name;
|
|
|
|
// Clear file input to prevent re-selection issues
|
|
if (fileInput.value) {
|
|
fileInput.value.value = '';
|
|
}
|
|
|
|
return { success: true, result: resultFile };
|
|
} catch (error) {
|
|
const errorMsg = error instanceof Error ? error.message : 'Processing failed';
|
|
errorMessage.value = errorMsg;
|
|
return { success: false, error: errorMsg };
|
|
} finally {
|
|
isProcessing.value = false;
|
|
}
|
|
}
|
|
|
|
// Handle file input selection
|
|
async function handleFileSelect(
|
|
files: FileList | null,
|
|
): Promise<{ success: boolean; result?: File; error?: string }> {
|
|
if (!files || files.length === 0) {
|
|
return { success: false, error: 'No files selected' };
|
|
}
|
|
|
|
const fileArray = Array.from(files);
|
|
let folderName: string | undefined;
|
|
|
|
if (config.mode === 'folder' && files[0].webkitRelativePath) {
|
|
folderName = files[0].webkitRelativePath.split('/')[0];
|
|
}
|
|
|
|
return await processFiles(fileArray, folderName);
|
|
}
|
|
|
|
// Handle drag and drop
|
|
function handleDragOver(event: DragEvent) {
|
|
event.preventDefault();
|
|
dragActive.value = true;
|
|
}
|
|
|
|
function handleDragLeave() {
|
|
dragActive.value = false;
|
|
}
|
|
|
|
async function handleDrop(event: DragEvent): Promise<{ success: boolean; result?: File; error?: string }> {
|
|
event.preventDefault();
|
|
dragActive.value = false;
|
|
|
|
if (config.mode === 'folder') {
|
|
return await handleFolderDrop(event);
|
|
}
|
|
return await handleFileSelect(event.dataTransfer?.files || null);
|
|
}
|
|
|
|
// Specialized folder drop handling
|
|
async function handleFolderDrop(event: DragEvent): Promise<{ success: boolean; result?: File; error?: string }> {
|
|
if (!event.dataTransfer?.items?.length) {
|
|
return { success: false, error: 'No items found' };
|
|
}
|
|
|
|
// Check directory reading capability
|
|
if (!('webkitGetAsEntry' in DataTransferItem.prototype)) {
|
|
const error = "Your browser doesn't support folder drop. Please use the browse button instead.";
|
|
errorMessage.value = error;
|
|
return { success: false, error };
|
|
}
|
|
|
|
const entry = event.dataTransfer.items[0].webkitGetAsEntry();
|
|
if (!entry?.isDirectory) {
|
|
const error = 'Please drop a folder, not a file.';
|
|
errorMessage.value = error;
|
|
return { success: false, error };
|
|
}
|
|
|
|
try {
|
|
const files = await collectFilesFromEntry(entry);
|
|
return await processFiles(files, entry.name);
|
|
} catch {
|
|
const errorMsg = 'Failed to process the folder. Please try again or use the browse button.';
|
|
errorMessage.value = errorMsg;
|
|
return { success: false, error: errorMsg };
|
|
}
|
|
}
|
|
|
|
// Constants for safety limits
|
|
const MAX_DEPTH = 20;
|
|
const MAX_FILES = 10000;
|
|
|
|
// Helper functions for folder processing
|
|
async function collectFilesFromEntry(
|
|
entry: FileSystemEntry,
|
|
path = '',
|
|
depth = 0,
|
|
fileCount = { current: 0 },
|
|
): Promise<File[]> {
|
|
// Check depth limit
|
|
if (depth > MAX_DEPTH) {
|
|
throw new Error(`Directory structure too deep (max depth: ${MAX_DEPTH})`);
|
|
}
|
|
|
|
// Check file count limit
|
|
if (fileCount.current > MAX_FILES) {
|
|
throw new Error(`Too many files in directory structure (max files: ${MAX_FILES})`);
|
|
}
|
|
|
|
if (entry.isFile) {
|
|
return new Promise((resolve, reject) => {
|
|
(entry as FileSystemFileEntry).file((file: File) => {
|
|
// Check file count before adding
|
|
if (fileCount.current >= MAX_FILES) {
|
|
reject(new Error(`Too many files in directory structure (max files: ${MAX_FILES})`));
|
|
return;
|
|
}
|
|
|
|
fileCount.current++;
|
|
|
|
const customFile = new File([file], file.name, {
|
|
type: file.type,
|
|
lastModified: file.lastModified,
|
|
});
|
|
|
|
Object.defineProperty(customFile, 'webkitRelativePath', {
|
|
value: path ? `${path}/${file.name}` : file.name,
|
|
});
|
|
|
|
resolve([customFile]);
|
|
}, reject);
|
|
});
|
|
}
|
|
|
|
if (entry.isDirectory && (entry as FileSystemDirectoryEntry).createReader) {
|
|
return new Promise((resolve, reject) => {
|
|
const dirReader = (entry as FileSystemDirectoryEntry).createReader();
|
|
const allFiles: File[] = [];
|
|
|
|
function readEntries() {
|
|
dirReader.readEntries(async (entries: FileSystemEntry[]) => {
|
|
if (entries.length === 0) {
|
|
resolve(allFiles);
|
|
} else {
|
|
try {
|
|
for (const childEntry of entries) {
|
|
// Check limits before processing each entry
|
|
if (depth + 1 > MAX_DEPTH) {
|
|
throw new Error(`Directory structure too deep (max depth: ${MAX_DEPTH})`);
|
|
}
|
|
if (fileCount.current > MAX_FILES) {
|
|
throw new Error(`Too many files in directory structure (max files: ${MAX_FILES})`);
|
|
}
|
|
|
|
const newPath = path ? `${path}/${childEntry.name}` : childEntry.name;
|
|
const files = await collectFilesFromEntry(childEntry, newPath, depth + 1, fileCount);
|
|
allFiles.push(...files);
|
|
}
|
|
readEntries();
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
}
|
|
}, reject);
|
|
}
|
|
|
|
readEntries();
|
|
});
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
// Trigger file input
|
|
function triggerFileInput() {
|
|
fileInput.value?.click();
|
|
}
|
|
|
|
// Input attributes for template
|
|
const inputAttributes = computed(() => ({
|
|
type: 'file',
|
|
accept,
|
|
multiple,
|
|
webkitdirectory: webkitdirectory || config.mode === 'folder',
|
|
...(config.mode === 'folder' && {
|
|
directory: true,
|
|
mozdirectory: true,
|
|
}),
|
|
}));
|
|
|
|
return {
|
|
// Refs
|
|
fileInput,
|
|
dragActive,
|
|
selectedItem,
|
|
errorMessage,
|
|
isProcessing,
|
|
|
|
// Computed
|
|
hasError,
|
|
hasSelection,
|
|
isValid,
|
|
inputAttributes,
|
|
|
|
// Methods
|
|
handleFileSelect,
|
|
handleDragOver,
|
|
handleDragLeave,
|
|
handleDrop,
|
|
triggerFileInput,
|
|
clearError,
|
|
clearSelection,
|
|
processFiles,
|
|
};
|
|
}
|