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.
245 lines
5.6 KiB
JavaScript
245 lines
5.6 KiB
JavaScript
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
|
|
};
|