feat: Implement image fetching and storage system
Some checks failed
Deploy to BeePC / deploy (push) Has been cancelled

- Add image-fetcher module for downloading and saving images from various sources.
- Create storage module for managing image files, including downloading, verifying integrity, and cleaning up orphaned files.
- Develop gallery HTML page for displaying images with sorting and filtering options.
- Set up RESTful API routes for image management, including fetching, adding tags, and deleting images.
- Introduce setup script for initializing the database and configuring image sources.
- Implement a batch process for verifying image integrity and cleaning up old images.
- Add setup batch script for easy installation and configuration of the image storage system.
This commit is contained in:
2026-02-12 13:13:36 -05:00
parent ea6cc3fc85
commit 9c72b00b1b
19 changed files with 3004 additions and 71 deletions

244
lib/storage.js Normal file
View File

@@ -0,0 +1,244 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const IMAGES_DIR = path.join(__dirname, '..', 'data', 'images');
// Ensure images directory exists
if (!fs.existsSync(IMAGES_DIR)) {
fs.mkdirSync(IMAGES_DIR, { recursive: true });
}
/**
* Calculate SHA256 hash of a file
*/
function calculateFileHash(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('error', reject);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
/**
* Generate a unique filename
*/
function generateFilename(sourceUrl, timestamp = Date.now()) {
const hash = crypto.createHash('md5').update(sourceUrl + timestamp).digest('hex');
const ext = getFileExtension(sourceUrl);
return `image_${timestamp}_${hash}${ext}`;
}
/**
* Get file extension from URL
*/
function getFileExtension(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const ext = path.extname(pathname);
// If no extension, try to get from content-type or use default
if (!ext || ext.length > 5) {
return '.jpg';
}
return ext;
} catch (err) {
return '.jpg';
}
}
/**
* Download an image and save it to storage
*/
async function downloadAndSaveImage(sourceUrl, options = {}) {
const { timeout = 30000, maxSize = 50 * 1024 * 1024 } = options;
try {
// Download image
const response = await axios({
url: sourceUrl,
method: 'GET',
responseType: 'arraybuffer',
timeout,
maxContentLength: maxSize,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
const filename = generateFilename(sourceUrl);
const filePath = path.join(IMAGES_DIR, filename);
const fileBuffer = Buffer.from(response.data);
const filesize = fileBuffer.length;
const file_hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const mime_type = response.headers['content-type'] || 'image/jpeg';
// Save file
fs.writeFileSync(filePath, fileBuffer);
return {
filename,
file_path: filePath,
relative_path: path.relative(path.join(__dirname, '..'), filePath),
filesize,
file_hash,
mime_type,
source_url: sourceUrl,
success: true
};
} catch (err) {
console.error(`Failed to download image from ${sourceUrl}:`, err.message);
return {
source_url: sourceUrl,
success: false,
error: err.message
};
}
}
/**
* Verify image integrity using stored hash
*/
async function verifyImageIntegrity(filePath, expectedHash) {
try {
if (!fs.existsSync(filePath)) {
return { valid: false, reason: 'File does not exist' };
}
const actualHash = await calculateFileHash(filePath);
if (actualHash !== expectedHash) {
return { valid: false, reason: 'Hash mismatch', expectedHash, actualHash };
}
return { valid: true };
} catch (err) {
return { valid: false, reason: err.message };
}
}
/**
* Delete image file
*/
function deleteImageFile(filePath) {
return new Promise((resolve, reject) => {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
resolve(true);
} else {
resolve(false);
}
} catch (err) {
reject(err);
}
});
}
/**
* Get image file as buffer
*/
function getImageBuffer(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
/**
* Get storage statistics
*/
function getStorageStats() {
return new Promise((resolve, reject) => {
try {
let totalSize = 0;
let fileCount = 0;
if (!fs.existsSync(IMAGES_DIR)) {
resolve({ totalSize: 0, fileCount: 0, dataDir: IMAGES_DIR });
return;
}
const files = fs.readdirSync(IMAGES_DIR);
files.forEach((file) => {
const filePath = path.join(IMAGES_DIR, file);
const stats = fs.statSync(filePath);
if (stats.isFile()) {
totalSize += stats.size;
fileCount++;
}
});
resolve({
totalSize,
totalSizeGB: (totalSize / (1024 ** 3)).toFixed(2),
fileCount,
dataDir: IMAGES_DIR
});
} catch (err) {
reject(err);
}
});
}
/**
* Clean up orphaned files (files in storage but not in database)
*/
async function cleanupOrphanedFiles(db) {
try {
if (!fs.existsSync(IMAGES_DIR)) {
return { cleaned: 0 };
}
const files = fs.readdirSync(IMAGES_DIR);
let cleaned = 0;
for (const file of files) {
const filePath = path.join(IMAGES_DIR, file);
// Check if file exists in database
const existsInDb = await new Promise((resolve) => {
db.get(
'SELECT id FROM images WHERE filename = ?',
[file],
(err, row) => resolve(!err && !!row)
);
});
if (!existsInDb) {
try {
fs.unlinkSync(filePath);
cleaned++;
} catch (err) {
console.error(`Failed to delete orphaned file ${file}:`, err.message);
}
}
}
return { cleaned };
} catch (err) {
console.error('Cleanup error:', err);
return { cleaned: 0, error: err.message };
}
}
module.exports = {
downloadAndSaveImage,
verifyImageIntegrity,
deleteImageFile,
getImageBuffer,
getStorageStats,
calculateFileHash,
generateFilename,
getFileExtension,
cleanupOrphanedFiles,
IMAGES_DIR
};