image-asset-manager
Version:
A comprehensive image asset management tool for frontend projects
261 lines • 11.8 kB
JavaScript
;
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