UNPKG

image-asset-manager

Version:

A comprehensive image asset management tool for frontend projects

261 lines 11.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.FileScannerImpl = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); const types_1 = require("../types"); const constants_1 = require("../constants"); const MetadataExtractor_1 = require("./MetadataExtractor"); class FileScannerImpl { constructor() { this.metadataExtractor = new MetadataExtractor_1.MetadataExtractor(); } static generateId(filePath) { return crypto .createHash("md5") .update(filePath) .digest("hex") .substring(0, 8); } static async calculateFileHash(filePath) { try { const fileBuffer = await fs.readFile(filePath); return crypto.createHash("sha256").update(fileBuffer).digest("hex"); } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to calculate hash for file: ${filePath}`, error, true); } } static isImageFile(filePath) { const ext = path.extname(filePath).toLowerCase(); return constants_1.SUPPORTED_IMAGE_EXTENSIONS.includes(ext); } static shouldExclude(filePath, excludePatterns) { const normalizedPath = filePath.replace(/\\/g, "/"); return excludePatterns.some((pattern) => { // Simple pattern matching - can be enhanced with glob patterns later if (pattern.includes("*")) { const regex = new RegExp(pattern.replace(/\*/g, ".*")); return regex.test(normalizedPath); } return normalizedPath.includes(pattern); }); } static categorizeImage(filePath) { const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/"); // Simple categorization based on path patterns if (normalizedPath.includes("icon")) return "icons"; if (normalizedPath.includes("logo")) return "logos"; if (normalizedPath.includes("avatar") || normalizedPath.includes("profile")) return "avatars"; if (normalizedPath.includes("background") || normalizedPath.includes("bg")) return "backgrounds"; if (normalizedPath.includes("banner") || normalizedPath.includes("hero")) return "banners"; if (normalizedPath.includes("thumb") || normalizedPath.includes("preview")) return "thumbnails"; if (normalizedPath.includes("gallery") || normalizedPath.includes("photo")) return "gallery"; if (normalizedPath.includes("ui") || normalizedPath.includes("component")) return "ui-components"; return "general"; } async scanDirectory(projectPath, options) { const imageFiles = []; try { await this.scanDirectoryRecursive(projectPath, projectPath, imageFiles, options, 0); // Extract metadata for all found images await this.extractMetadataForFiles(imageFiles); return imageFiles; } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to scan directory: ${projectPath}`, error, true); } } /** * Extract metadata for all image files and update their properties */ async extractMetadataForFiles(files) { // Process files in batches to avoid overwhelming the system const batchSize = 10; let processed = 0; for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); const promises = batch.map(async (file) => { try { const metadata = await this.metadataExtractor.extractMetadata(file); // Update category based on metadata if needed const enhancedCategory = this.metadataExtractor.categorizeImageWithMetadata(file, metadata); if (enhancedCategory !== "general" && file.category === "general") { file.category = enhancedCategory; } processed++; } catch (error) { console.warn(`⚠️ Failed to extract metadata for ${file.name}:`, error instanceof Error ? error.message : String(error)); // Keep the file but without enhanced metadata // Ensure dimensions are at least set to 0 if extraction failed if (!file.dimensions || (file.dimensions.width === 0 && file.dimensions.height === 0)) { console.log(`🔧 Attempting direct metadata extraction for ${file.name}...`); try { const sharp = require("sharp"); const metadata = await sharp(file.path).metadata(); file.dimensions = { width: metadata.width || 0, height: metadata.height || 0, }; } catch (directError) { console.warn(`❌ Direct extraction also failed for ${file.name}`); } } } }); await Promise.all(promises); } } async scanDirectoryRecursive(currentPath, basePath, imageFiles, options, currentDepth) { // Check depth limit if (options.maxDepth > 0 && currentDepth >= options.maxDepth) { return; } try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); const relativePath = path.relative(basePath, fullPath); // Check if should be excluded if (FileScannerImpl.shouldExclude(relativePath, options.excludePatterns)) { continue; } if (entry.isDirectory() && options.recursive) { await this.scanDirectoryRecursive(fullPath, basePath, imageFiles, options, currentDepth + 1); } else if (entry.isFile() && FileScannerImpl.isImageFile(fullPath)) { try { const imageFile = await this.createImageFile(fullPath, basePath); imageFiles.push(imageFile); } catch (error) { // Log warning but continue scanning console.warn(`Warning: Failed to process image file ${fullPath}:`, error); } } } } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.PERMISSION_DENIED, `Permission denied or directory not found: ${currentPath}`, error, true); } } async createImageFile(filePath, basePath) { try { const stats = await fs.stat(filePath); const relativePath = path.relative(basePath, filePath); const hash = await FileScannerImpl.calculateFileHash(filePath); return { id: FileScannerImpl.generateId(filePath), path: filePath, relativePath: relativePath.replace(/\\/g, "/"), // Normalize path separators name: path.basename(filePath, path.extname(filePath)), extension: path.extname(filePath).toLowerCase(), size: stats.size, hash, dimensions: { width: 0, height: 0 }, // Will be populated by metadata extractor createdAt: stats.birthtime, modifiedAt: stats.mtime, category: FileScannerImpl.categorizeImage(relativePath), }; } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.INVALID_IMAGE_FORMAT, `Failed to create image file object for: ${filePath}`, error, true); } } async detectDuplicates(files) { const hashGroups = new Map(); // Group files by hash for (const file of files) { if (!hashGroups.has(file.hash)) { hashGroups.set(file.hash, []); } hashGroups.get(file.hash).push(file); } // Find groups with more than one file (duplicates) const duplicateGroups = []; for (const [hash, groupFiles] of hashGroups) { if (groupFiles.length > 1) { const totalSize = groupFiles.reduce((sum, file) => sum + file.size, 0); const recommendation = this.generateDuplicateRecommendation(groupFiles); duplicateGroups.push({ hash, files: groupFiles, totalSize, recommendation, }); } } return duplicateGroups; } generateDuplicateRecommendation(files) { // Sort by preference: shorter path, more descriptive name, newer file const sortedFiles = [...files].sort((a, b) => { // Prefer shorter paths (likely more organized) const pathDiff = a.relativePath.split("/").length - b.relativePath.split("/").length; if (pathDiff !== 0) return pathDiff; // Prefer more descriptive names (longer names often more descriptive) const nameDiff = b.name.length - a.name.length; if (nameDiff !== 0) return nameDiff; // Prefer newer files return b.modifiedAt.getTime() - a.modifiedAt.getTime(); }); const preferred = sortedFiles[0]; const duplicates = sortedFiles.slice(1); return `Keep "${preferred.relativePath}" and consider removing: ${duplicates .map((f) => f.relativePath) .join(", ")}`; } watchChanges(callback) { // File watching will be implemented in a separate FileWatcher class // This is a placeholder for the interface compliance } } exports.FileScannerImpl = FileScannerImpl; //# sourceMappingURL=FileScanner.js.map