442 lines
12 KiB
JavaScript
442 lines
12 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const router = express.Router();
|
|
const database = require('../lib/database');
|
|
const storage = require('../lib/storage');
|
|
const imageFetcher = require('../lib/image-fetcher');
|
|
|
|
const CONFIG_PATH = path.join(__dirname, '..', 'image-sources.json');
|
|
|
|
function readSourcesConfig() {
|
|
const defaultConfig = { sources: [], fetchInterval: 2.5 };
|
|
try {
|
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
return defaultConfig;
|
|
}
|
|
|
|
const content = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
const parsed = JSON.parse(content);
|
|
return {
|
|
sources: Array.isArray(parsed.sources) ? parsed.sources : [],
|
|
fetchInterval: typeof parsed.fetchInterval === 'number' ? parsed.fetchInterval : 2.5,
|
|
comments: parsed.comments || undefined
|
|
};
|
|
} catch (err) {
|
|
console.warn('Failed to read image-sources.json:', err.message);
|
|
return defaultConfig;
|
|
}
|
|
}
|
|
|
|
function writeSourcesConfig(config) {
|
|
const payload = {
|
|
...config,
|
|
comments: config.comments || {
|
|
sources: 'Array of image sources to fetch from',
|
|
name: 'Human-readable name for the source',
|
|
url: 'Full URL to the image',
|
|
tags: 'Array of tags to apply to fetched images (useful for ML training)',
|
|
enabled: 'Set to true to include this source in fetching',
|
|
fetchInterval: 'Minutes between fetch cycles. Examples: 0.033 = 2 seconds, 0.05 = 3 seconds, 0.083 = 5 seconds, 1 = 1 minute, 2.5 = 2.5 minutes (default)'
|
|
}
|
|
};
|
|
|
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(payload, null, 2));
|
|
}
|
|
|
|
function restartFetcherFromConfig(config) {
|
|
imageFetcher.stopFetcher();
|
|
const enabledSources = (config.sources || []).filter((source) => source.enabled);
|
|
const interval = config.fetchInterval || 2.5;
|
|
if (enabledSources.length > 0) {
|
|
imageFetcher.startFetcher(enabledSources, interval);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/images - Get all images with optional filtering
|
|
*/
|
|
router.get('/images', async (req, res) => {
|
|
try {
|
|
const { tag, sourceUrl, page = 1, pageSize = 50, sort = 'fetched_at', order = 'DESC' } = req.query;
|
|
|
|
const limit = parseInt(pageSize);
|
|
const offset = (parseInt(page) - 1) * limit;
|
|
|
|
const images = await database.getImages({
|
|
tag,
|
|
sourceUrl,
|
|
limit,
|
|
offset,
|
|
sortBy: sort,
|
|
order
|
|
});
|
|
|
|
// Get tags for each image
|
|
for (let i = 0; i < images.length; i++) {
|
|
images[i].tags = await database.getImageTags(images[i].id);
|
|
}
|
|
|
|
const total = await database.getImageCount(tag);
|
|
|
|
res.json({
|
|
success: true,
|
|
images,
|
|
pagination: {
|
|
page: parseInt(page),
|
|
pageSize: limit,
|
|
total,
|
|
pages: Math.ceil(total / limit)
|
|
}
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/images/:id - Get a specific image
|
|
*/
|
|
router.get('/images/:id', async (req, res) => {
|
|
try {
|
|
const image = await database.getImage(req.params.id);
|
|
|
|
if (!image) {
|
|
return res.status(404).json({ success: false, error: 'Image not found' });
|
|
}
|
|
|
|
image.tags = await database.getImageTags(image.id);
|
|
|
|
res.json({ success: true, image });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/images/:id/download - Download image file
|
|
*/
|
|
router.get('/images/:id/download', async (req, res) => {
|
|
try {
|
|
const image = await database.getImage(req.params.id);
|
|
|
|
if (!image) {
|
|
return res.status(404).json({ success: false, error: 'Image not found' });
|
|
}
|
|
|
|
const buffer = await storage.getImageBuffer(image.file_path);
|
|
res.contentType(image.mime_type);
|
|
res.send(buffer);
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/images - Fetch and save a new image
|
|
*/
|
|
router.post('/images', async (req, res) => {
|
|
try {
|
|
const { source_url, tags = [] } = req.body;
|
|
|
|
if (!source_url) {
|
|
return res.status(400).json({ success: false, error: 'source_url is required' });
|
|
}
|
|
|
|
const result = await imageFetcher.fetchImage(source_url, tags);
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({ success: false, error: result.error });
|
|
}
|
|
|
|
const image = await database.getImage(result.imageId);
|
|
image.tags = await database.getImageTags(image.id);
|
|
|
|
res.status(201).json({ success: true, image });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/images/:id/tags - Add tags to an image
|
|
*/
|
|
router.post('/images/:id/tags', async (req, res) => {
|
|
try {
|
|
const { tags = [] } = req.body;
|
|
|
|
if (!Array.isArray(tags) || tags.length === 0) {
|
|
return res.status(400).json({ success: false, error: 'tags must be a non-empty array' });
|
|
}
|
|
|
|
const image = await database.getImage(req.params.id);
|
|
if (!image) {
|
|
return res.status(404).json({ success: false, error: 'Image not found' });
|
|
}
|
|
|
|
await database.addTags(image.id, tags);
|
|
|
|
image.tags = await database.getImageTags(image.id);
|
|
|
|
res.json({ success: true, image });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/images/:id - Delete an image
|
|
*/
|
|
router.delete('/images/:id', async (req, res) => {
|
|
try {
|
|
const image = await database.getImage(req.params.id);
|
|
|
|
if (!image) {
|
|
return res.status(404).json({ success: false, error: 'Image not found' });
|
|
}
|
|
|
|
// Delete file
|
|
await storage.deleteImageFile(image.file_path);
|
|
|
|
// Delete database record
|
|
await database.deleteImage(image.id);
|
|
|
|
res.json({ success: true, message: 'Image deleted' });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/tags - Get all available tags
|
|
*/
|
|
router.get('/tags', async (req, res) => {
|
|
try {
|
|
const tags = await database.getAllTags();
|
|
res.json({ success: true, tags });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/stats - Get storage and image statistics
|
|
*/
|
|
router.get('/stats', async (req, res) => {
|
|
try {
|
|
const storageStats = await storage.getStorageStats();
|
|
const imageCount = await database.getImageCount();
|
|
|
|
res.json({
|
|
success: true,
|
|
stats: {
|
|
imageCount,
|
|
...storageStats
|
|
}
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/verify - Verify all images for corruption
|
|
*/
|
|
router.post('/verify', async (req, res) => {
|
|
try {
|
|
const result = await imageFetcher.verifyAllImages();
|
|
res.json({ success: true, result });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/cleanup - Clean up old images
|
|
*/
|
|
router.post('/cleanup', async (req, res) => {
|
|
try {
|
|
const { daysOld = 30 } = req.body;
|
|
|
|
const deleted = await database.cleanupOldImages(daysOld);
|
|
const orphaned = await storage.cleanupOrphanedFiles(database.db);
|
|
|
|
res.json({
|
|
success: true,
|
|
cleanup: {
|
|
deletedFromDb: deleted,
|
|
orphanedFilesRemoved: orphaned.cleaned
|
|
}
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/cleanup/before - Delete images before a specific date
|
|
*/
|
|
router.post('/cleanup/before', async (req, res) => {
|
|
try {
|
|
const { beforeDate } = req.body;
|
|
|
|
if (!beforeDate || typeof beforeDate !== 'string') {
|
|
return res.status(400).json({ success: false, error: 'beforeDate is required (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)' });
|
|
}
|
|
|
|
const normalizedInput = beforeDate.replace('T', ' ');
|
|
const normalized = normalizedInput.length <= 10
|
|
? `${normalizedInput} 00:00:00`
|
|
: (normalizedInput.length === 16 ? `${normalizedInput}:00` : normalizedInput);
|
|
|
|
const imagesToDelete = await database.getImagesBeforeDate(normalized);
|
|
let filesDeleted = 0;
|
|
|
|
for (const image of imagesToDelete) {
|
|
const deletedFile = await storage.deleteImageFile(image.file_path);
|
|
if (deletedFile) filesDeleted++;
|
|
}
|
|
|
|
const deletedFromDb = await database.deleteImagesBeforeDate(normalized);
|
|
|
|
res.json({
|
|
success: true,
|
|
cleanup: {
|
|
deletedFromDb,
|
|
filesDeleted,
|
|
beforeDate: normalized
|
|
}
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/fetcher/status - Get fetcher status
|
|
*/
|
|
router.get('/fetcher/status', (req, res) => {
|
|
const status = imageFetcher.getStatus();
|
|
res.json({ success: true, status });
|
|
});
|
|
|
|
/**
|
|
* GET /api/sources - Get image sources configuration
|
|
*/
|
|
router.get('/sources', (req, res) => {
|
|
const config = readSourcesConfig();
|
|
res.json({ success: true, config });
|
|
});
|
|
|
|
/**
|
|
* POST /api/sources - Add a new image source
|
|
*/
|
|
router.post('/sources', (req, res) => {
|
|
try {
|
|
const { name, url, tags = [], enabled = true } = req.body;
|
|
|
|
if (!name || !url) {
|
|
return res.status(400).json({ success: false, error: 'name and url are required' });
|
|
}
|
|
|
|
const config = readSourcesConfig();
|
|
const newSource = {
|
|
name: String(name).trim(),
|
|
url: String(url).trim(),
|
|
tags: Array.isArray(tags) ? tags.map(tag => String(tag).trim()).filter(Boolean) : [],
|
|
enabled: Boolean(enabled)
|
|
};
|
|
|
|
config.sources = [...(config.sources || []), newSource];
|
|
writeSourcesConfig(config);
|
|
restartFetcherFromConfig(config);
|
|
|
|
res.status(201).json({ success: true, config });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/sources/config - Update fetch interval
|
|
*/
|
|
router.put('/sources/config', (req, res) => {
|
|
try {
|
|
const { fetchInterval } = req.body;
|
|
const intervalValue = Number(fetchInterval);
|
|
|
|
if (!Number.isFinite(intervalValue) || intervalValue <= 0) {
|
|
return res.status(400).json({ success: false, error: 'fetchInterval must be a positive number' });
|
|
}
|
|
|
|
const config = readSourcesConfig();
|
|
config.fetchInterval = intervalValue;
|
|
writeSourcesConfig(config);
|
|
restartFetcherFromConfig(config);
|
|
|
|
res.json({ success: true, config });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/sources/:index - Update an existing source
|
|
*/
|
|
router.put('/sources/:index', (req, res) => {
|
|
try {
|
|
const index = Number(req.params.index);
|
|
if (!Number.isInteger(index)) {
|
|
return res.status(400).json({ success: false, error: 'Invalid source index' });
|
|
}
|
|
|
|
const config = readSourcesConfig();
|
|
if (!config.sources || !config.sources[index]) {
|
|
return res.status(404).json({ success: false, error: 'Source not found' });
|
|
}
|
|
|
|
const current = config.sources[index];
|
|
const updated = {
|
|
...current,
|
|
name: req.body.name !== undefined ? String(req.body.name).trim() : current.name,
|
|
url: req.body.url !== undefined ? String(req.body.url).trim() : current.url,
|
|
tags: Array.isArray(req.body.tags)
|
|
? req.body.tags.map(tag => String(tag).trim()).filter(Boolean)
|
|
: current.tags,
|
|
enabled: req.body.enabled !== undefined ? Boolean(req.body.enabled) : current.enabled
|
|
};
|
|
|
|
config.sources[index] = updated;
|
|
writeSourcesConfig(config);
|
|
restartFetcherFromConfig(config);
|
|
|
|
res.json({ success: true, config });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/sources/:index - Remove a source
|
|
*/
|
|
router.delete('/sources/:index', (req, res) => {
|
|
try {
|
|
const index = Number(req.params.index);
|
|
const config = readSourcesConfig();
|
|
|
|
if (!Number.isInteger(index) || !config.sources || !config.sources[index]) {
|
|
return res.status(404).json({ success: false, error: 'Source not found' });
|
|
}
|
|
|
|
config.sources.splice(index, 1);
|
|
writeSourcesConfig(config);
|
|
restartFetcherFromConfig(config);
|
|
|
|
res.json({ success: true, config });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|