Compare commits

..

3 Commits

Author SHA1 Message Date
Gitea e347c4baf6 first 2026-06-24 16:14:27 +05:30
Gitea f9cd469db9 first commit 2026-06-24 16:10:54 +05:30
Gitea e6628183b0 Initialize project using Create React App 2026-06-22 13:32:16 +05:30
37 changed files with 24 additions and 3862 deletions
+1
View File
@@ -0,0 +1 @@
REACT_APP_API_URL=http://localhost:8981/api
+22 -2
View File
@@ -1,3 +1,23 @@
./client/node_module
./server/node_module
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
View File
-26
View File
@@ -1,26 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

-8
View File
@@ -1,8 +0,0 @@
PORT=5000
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
JWT_EXPIRE=7d
NODE_ENV=development
UPLOAD_FOLDER=uploads
OUTPUT_FOLDER=outputs
COMPRESSION_QUALITY=80
MAX_FILE_SIZE=50000000
-8
View File
@@ -1,8 +0,0 @@
node_modules/
.env
uploads/
outputs/
*.log
.DS_Store
dist/
build/
-72
View File
@@ -1,72 +0,0 @@
# Image Manager - Backend Server
Node.js/Express backend for the Image Manager System with JWT authentication and image compression.
## Features
- JWT-based authentication
- Role-based access control (HR, SLIP)
- Image upload and compression
- File management API
- ZIP file creation
- Auto-delete functionality
## Installation
```bash
npm install
```
## Configuration
Create a `.env` file with:
```
PORT=5000
JWT_SECRET=your_secret_key
JWT_EXPIRE=7d
NODE_ENV=development
MAX_FILE_SIZE=50000000
COMPRESSION_QUALITY=80
```
## Running
```bash
# Production
npm start
# Development with auto-reload
npm run dev
```
## API Endpoints
### Auth
- `POST /api/auth/signup` - Register
- `POST /api/auth/login` - Login
### Files
- `GET /api/files/list` - Get source images
- `GET /api/files/outputs` - Get compressed images
- `POST /api/files/upload` - Upload image
- `POST /api/files/upload-multiple` - Bulk upload
- `POST /api/files/compress/:filename` - Compress
- `POST /api/files/compress-multiple` - Batch compress
- `GET /api/files/download/:filename` - Download
- `POST /api/files/download-zip` - Download ZIP
- `DELETE /api/files/delete/:filename` - Delete
## Dependencies
- express
- cors
- jsonwebtoken
- bcryptjs
- sharp
- multer
- archiver
- dotenv
## Port
Default: 5000
-47
View File
@@ -1,47 +0,0 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import authRoutes from './routes/auth.js';
import fileRoutes from './routes/files.js';
import { authenticate } from './middleware/auth.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.set('trust proxy', true);
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// Serve static files
app.use('/outputs', express.static(path.join(__dirname, 'outputs')));
app.use('/source', express.static(path.join(__dirname, 'source')));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/files', authenticate, fileRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'Server is running' });
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
});
const PORT = process.env.PORT;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
-26
View File
@@ -1,26 +0,0 @@
import jwt from 'jsonwebtoken';
export const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
export const authorize = (roles = []) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
};
-2890
View File
File diff suppressed because it is too large Load Diff
-40
View File
@@ -1,40 +0,0 @@
{
"name": "image-manager-server",
"version": "1.0.0",
"description": "Backend for Image Management System with JWT Auth and Role-based Access",
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon index.js",
"dev": "nodemon index.js"
},
"keywords": [
"express",
"jwt",
"image",
"compression"
],
"author": "",
"license": "ISC",
"dependencies": {
"@napi-rs/canvas": "^1.0.0",
"archiver": "^6.0.0",
"bcryptjs": "^2.4.3",
"compress-pdf": "^0.6.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"fs-extra": "^11.3.5",
"jsonwebtoken": "^9.0.0",
"multer": "^2.2.0",
"pdf-lib": "^1.17.1",
"pdf-poppler": "^0.2.3",
"pdfjs-dist": "^6.0.227",
"pdfkit": "^0.19.1",
"sharp": "^0.32.6",
"uuid": "^9.0.0"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
-98
View File
@@ -1,98 +0,0 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
const router = express.Router();
// Mock database - replace with real DB in production
const users = [
{
id: 1,
username: 'Hr_admin',
email: 'hr@7823cpmindia.com',
password:'Hr@12345',
// password: '$2a$10$u0F8fM.6qz.2D0X0Z7.D6O9K2n0F8fM.6qz.2D0X0Z7.D6O9K2n0F8', // password123
role: 'HR'
},
{
id: 2,
username: 'slip_admin',
email: 'slip@company.com',
// password: '$2a$10$u0F8fM.6qz.2D0X0Z7.D6O9K2n0F8fM.6qz.2D0X0Z7.D6O9K2n0F8', // password123
password:'Hr@12345',
role: 'SLIP'
}
];
// Signup
router.post('/signup', async (req, res) => {
try {
const { username, email, password, role } = req.body;
if (!['HR', 'SLIP'].includes(role)) {
return res.status(400).json({ error: 'Invalid role' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = {
id: users.length + 1,
username,
email,
password: hashedPassword,
role
};
users.push(newUser);
const token = jwt.sign(
{ id: newUser.id, username, role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
res.status(201).json({
token,
user: { id: newUser.id, username, email, role }
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user) {
return res.status(400).json({ error: 'Invalid credentials' });
}
const password1 = users.find(u => u.password === password);
if (!password1) {
return res.status(400).json({ error: 'Invalid credentials' });
}
// const isPasswordValid = await bcrypt.compare(password, user.password);
// if (!isPasswordValid) {
// return res.status(400).json({ error: 'Invalid credentials' });
// }
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
res.json({
token,
user: { id: user.id, username: user.username, email: user.email, role: user.role }
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
export default router;
-644
View File
@@ -1,644 +0,0 @@
import express from 'express';
import path from 'path';
import fs from 'fs/promises';
import { fileURLToPath } from 'url';
import { execFile } from 'child_process';
import { promisify } from 'util';
import multer from 'multer';
import sharp from 'sharp';
import fsSync from 'fs';
import archiver from 'archiver';
import { v4 as uuidv4 } from 'uuid';
import { authorize } from '../middleware/auth.js';
import { createWriteStream } from 'fs';
import dotenv from 'dotenv';
dotenv.config();
import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib';
import { createCanvas } from '@napi-rs/canvas';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
const router = express.Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Create directories if they don't exist
const outputDir = path.join(__dirname, '..', 'outputs');
const sourceDir = path.join(__dirname, '..', 'source');
try {
await fs.mkdir(outputDir, { recursive: true });
await fs.mkdir(sourceDir, { recursive: true });
} catch (err) {
console.error('Error creating directories:', err);
}
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, sourceDir);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const allowedMimes = [
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
'application/pdf'
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
},
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 50000000 }
});
async function clearDirectory(dir) {
const items = await fs.readdir(dir);
await Promise.all(items.map(async (item) => {
const itemPath = path.join(dir, item);
await fs.unlink(itemPath);
}));
}
async function clearSourceDirectory(req, res, next) {
try {
await clearDirectory(sourceDir);
next();
} catch (err) {
next(err);
}
}
// Upload files into source folder, clearing it first
router.post('/upload-multiple', clearSourceDirectory, upload.array('images', 100), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
const uploadedFiles = req.files.map((file) => ({
name: file.originalname,
size: file.size,
url: `${baseUrl}/source/${encodeURIComponent(file.filename)}`
}));
res.json({
message: `${req.files.length} files uploaded successfully`,
files: uploadedFiles
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/upload', clearSourceDirectory, upload.single('image'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
res.json({
message: 'File uploaded successfully',
file: {
name: req.file.originalname,
size: req.file.size,
url: `${baseUrl}/source/${encodeURIComponent(req.file.filename)}`
}
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Get source files list from root folder
router.get('/list', async (req, res) => {
try {
const baseUrl = process.env.BASE_URL;
const files = await fs.readdir(sourceDir);
const fileDetails = await Promise.all(
files.map(async (file) => {
const filePath = path.join(sourceDir, file);
const stats = await fs.stat(filePath);
return {
name: file,
size: stats.size,
url: `${baseUrl}/source/${encodeURIComponent(file)}`,
uploadedAt: stats.mtime,
extension: path.extname(file).toLowerCase()
};
})
);
res.json({ files: fileDetails });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
async function compressPdf(inputPath, outputPath, quality = 60, targetKb = null) {
console.log('compressPdf called:', { inputPath, outputPath, quality, targetKb });
if (!fsSync.existsSync(inputPath)) {
throw new Error(`Input file not found: ${inputPath}`);
}
const outputDirPath = path.dirname(outputPath);
if (!fsSync.existsSync(outputDirPath)) {
throw new Error(`Output directory does not exist: ${outputDirPath}`);
}
const inputBytes = fsSync.readFileSync(inputPath);
const originalSize = inputBytes.length;
const targetBytes = targetKb ? targetKb * 1024 : null;
// Step 1: try image-recompression first (keeps text selectable where possible)
const firstAttempt = await tryImageRecompression(inputBytes, quality);
console.log(`Image-recompression attempt: ${firstAttempt.length} bytes`);
if (!targetBytes || firstAttempt.length <= targetBytes) {
fsSync.writeFileSync(outputPath, firstAttempt);
logResult(originalSize, firstAttempt.length, outputPath, targetBytes);
return;
}
// Step 2: target not met — rasterize pages to guarantee size control
console.log('Target not met via image recompression — falling back to page rasterization.');
const baseQuality = Math.max(15, Math.min(90, quality));
const attempts = [
{ dpi: 150, q: baseQuality },
{ dpi: 120, q: Math.max(15, baseQuality - 15) },
{ dpi: 96, q: Math.max(15, baseQuality - 30) },
{ dpi: 72, q: Math.max(15, baseQuality - 45) },
{ dpi: 72, q: 20 },
{ dpi: 50, q: 15 }
];
let bestBuffer = null;
for (const { dpi, q } of attempts) {
console.log(`Rasterizing at dpi=${dpi}, jpegQuality=${q}`);
const buffer = await rasterizeAndBuild(inputPath, dpi, q);
console.log(` -> ${buffer.length} bytes`);
if (!bestBuffer || buffer.length < bestBuffer.length) {
bestBuffer = buffer;
}
if (buffer.length <= targetBytes) {
fsSync.writeFileSync(outputPath, buffer);
console.log(`Target reached via rasterization at dpi=${dpi}, quality=${q}`);
logResult(originalSize, buffer.length, outputPath, targetBytes);
return;
}
}
console.warn(`Could not fully reach target of ${targetKb}KB. Writing smallest achieved version.`);
const finalBuffer = bestBuffer.length < firstAttempt.length ? bestBuffer : firstAttempt;
fsSync.writeFileSync(outputPath, finalBuffer);
logResult(originalSize, finalBuffer.length, outputPath, targetBytes);
}
// --- Step 1 helper: recompress existing embedded JPEG/Flate images in place ---
async function tryImageRecompression(inputBytes, quality) {
const pdfDoc = await PDFDocument.load(inputBytes, { ignoreEncryption: true, updateMetadata: false });
const context = pdfDoc.context;
const indirectObjects = context.enumerateIndirectObjects();
const scaleFactor = quality >= 80 ? 1 : quality >= 60 ? 0.85 : quality >= 40 ? 0.65 : 0.5;
for (const [ref, obj] of indirectObjects) {
try {
if (!(obj instanceof PDFRawStream)) continue;
const dict = obj.dict;
const subtype = dict.get(PDFName.of('Subtype'));
if (!subtype || subtype.toString() !== '/Image') continue;
const filter = dict.get(PDFName.of('Filter'));
const filterName = filter ? filter.toString() : '';
const isJpeg = filterName.includes('DCTDecode');
const isFlate = filterName.includes('FlateDecode');
if (!isJpeg && !isFlate) continue;
const rawBytes = obj.contents;
if (!rawBytes || rawBytes.length < 2000) continue;
let sharpInput;
if (isJpeg) {
sharpInput = Buffer.from(rawBytes);
} else {
const width = dict.get(PDFName.of('Width'))?.asNumber?.();
const height = dict.get(PDFName.of('Height'))?.asNumber?.();
const bpc = dict.get(PDFName.of('BitsPerComponent'))?.asNumber?.() || 8;
const csObj = dict.get(PDFName.of('ColorSpace'));
const csName = csObj ? csObj.toString() : '/DeviceRGB';
if (!width || !height || bpc !== 8) continue;
let channels = 3;
if (csName.includes('Gray')) channels = 1;
else if (csName.includes('CMYK')) channels = 4;
const expectedSize = width * height * channels;
if (rawBytes.length < expectedSize) continue;
sharpInput = await sharp(Buffer.from(rawBytes), { raw: { width, height, channels } }).toBuffer();
}
const image = sharp(sharpInput, { failOn: 'none' });
const metadata = await image.metadata();
const newWidth = Math.round((metadata.width || 0) * scaleFactor);
const compressedBuffer = await image
.resize({ width: newWidth > 0 ? newWidth : undefined, withoutEnlargement: true })
.jpeg({ quality: Math.max(10, Math.min(95, quality)), mozjpeg: true })
.toBuffer();
if (compressedBuffer.length >= rawBytes.length) continue;
const newMeta = await sharp(compressedBuffer).metadata();
dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
dict.set(PDFName.of('Width'), context.obj(newMeta.width));
dict.set(PDFName.of('Height'), context.obj(newMeta.height));
dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceRGB'));
dict.set(PDFName.of('BitsPerComponent'), context.obj(8));
dict.delete(PDFName.of('DecodeParms'));
dict.delete(PDFName.of('SMask'));
context.assign(ref, PDFRawStream.of(dict, compressedBuffer));
} catch (e) {
continue;
}
}
return await pdfDoc.save({ useObjectStreams: true });
}
// --- Step 2 helper: render every page to a JPEG via pdfjs-dist + napi-rs canvas, rebuild PDF ---
async function rasterizeAndBuild(inputPath, dpi, jpegQuality) {
const data = new Uint8Array(fsSync.readFileSync(inputPath));
const loadingTask = pdfjsLib.getDocument({ data });
const pdfDocument = await loadingTask.promise;
const newPdf = await PDFDocument.create();
const scale = dpi / 72; // pdfjs default is 72 DPI baseline
for (let pageNum = 1; pageNum <= pdfDocument.numPages; pageNum++) {
const page = await pdfDocument.getPage(pageNum);
const viewport = page.getViewport({ scale });
const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
const ctx = canvas.getContext('2d');
await page.render({
canvasContext: ctx,
viewport
}).promise;
const pngBuffer = canvas.toBuffer('image/png');
const compressed = await sharp(pngBuffer)
.jpeg({ quality: jpegQuality, mozjpeg: true })
.toBuffer();
const metadata = await sharp(compressed).metadata();
const jpgImage = await newPdf.embedJpg(compressed);
const newPage = newPdf.addPage([metadata.width, metadata.height]);
newPage.drawImage(jpgImage, {
x: 0,
y: 0,
width: metadata.width,
height: metadata.height
});
}
return await newPdf.save({ useObjectStreams: true });
}
function logResult(originalSize, newSize, outputPath, targetBytes) {
if (!fsSync.existsSync(outputPath) || fsSync.statSync(outputPath).size === 0) {
throw new Error(`Compression ran but produced no/empty output at ${outputPath}`);
}
const pct = ((1 - newSize / originalSize) * 100).toFixed(1);
console.log(`Done. ${originalSize} -> ${newSize} bytes (${pct}% reduction)`);
if (targetBytes && newSize > targetBytes) {
console.warn(`Note: final size (${newSize} bytes) still exceeds target (${targetBytes} bytes).`);
}
}
// Compress multiple files from source folder
router.post('/compress-multiple', async (req, res) => {
try {
const { filenames, quality, targetKb } = req.body;
if (!filenames || !Array.isArray(filenames) || filenames.length === 0) {
return res.status(400).json({ error: 'No filenames provided' });
}
const desiredKb = targetKb ? parseInt(targetKb, 10) : null;
const compressedFiles = await Promise.all(
filenames.map(async (filename) => {
const inputPath = path.join(sourceDir, filename);
const outputFilename = filename
const outputPath = path.join(outputDir, outputFilename);
const extension = path.extname(filename).toLowerCase();
try {
await fs.access(inputPath);
if (extension === '.pdf') {
await compressPdf(inputPath, outputPath, parseInt(quality, 10),targetKb);
} else {
let currentQuality = parseInt(quality, 10) || 80;
let sizeOk = false;
if (desiredKb) {
while (currentQuality >= 30) {
await sharp(inputPath)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: currentQuality, progressive: true })
.toFile(outputPath);
const stats = await fs.stat(outputPath);
if (stats.size <= desiredKb * 1024) {
sizeOk = true;
break;
}
currentQuality -= 5;
}
if (!sizeOk) {
// keep the smallest generated version
}
} else {
await sharp(inputPath)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: currentQuality, progressive: true })
.toFile(outputPath);
}
}
const stats = await fs.stat(outputPath);
return {
original: filename,
compressed: outputFilename,
url: `/outputs/${outputFilename}`,
size: stats.size
};
} catch (err) {
return { original: filename, error: err.message };
}
})
);
res.json({
message: 'Files compressed successfully',
files: compressedFiles
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Compress single file from source folder
router.post('/compress/:filename', async (req, res) => {
try {
const { filename } = req.params;
const { quality, targetKb } = req.body;
const inputPath = path.join(sourceDir, filename);
const outputFilename = filename;
const outputPath = path.join(outputDir, outputFilename);
await fs.access(inputPath);
const extension = path.extname(filename).toLowerCase();
if (extension === '.pdf') {
await compressPdf(inputPath, outputPath, parseInt(quality, 10),targetKb);
} else {
const desiredKb = targetKb ? parseInt(targetKb, 10) : null;
let currentQuality = parseInt(quality, 10)
if (desiredKb) {
let sizeOk = false;
while (currentQuality >= 30) {
await sharp(inputPath)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: currentQuality, progressive: true })
.toFile(outputPath);
const stats = await fs.stat(outputPath);
if (stats.size <= desiredKb * 1024) {
sizeOk = true;
break;
}
currentQuality -= 5;
}
if (!sizeOk) {
// Keep the smallest version generated
}
} else {
await sharp(inputPath)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: currentQuality, progressive: true })
.toFile(outputPath);
}
}
const stats = await fs.stat(outputPath);
res.json({
message: 'File compressed successfully',
file: {
filename: outputFilename,
url: `/outputs/${outputFilename}`,
size: stats.size
}
});
} catch (err) {
if (err.code === 'ENOENT') {
return res.status(404).json({ error: 'Input file not found' });
}
res.status(500).json({ error: err.message });
}
});
// Download single file
router.get('/download/:filename', async (req, res) => {
try {
const { filename } = req.params;
const filePath = path.join(outputDir, filename);
await fs.access(filePath);
res.download(filePath, filename, async (err) => {
if (err && err.code !== 'ERR_HTTP_HEADERS_SENT') {
console.error('Download error:', err);
} else {
// Auto-delete the downloaded output file
try {
await fs.unlink(filePath);
console.log(`File deleted: ${filename}`);
} catch (delErr) {
console.error('Delete error:', delErr);
}
// ALSO clear the source folder so both source + output end up empty
try {
await clearDirectory(sourceDir);
console.log('Source directory cleared after single file download');
} catch (srcErr) {
console.error('Could not clear source directory:', srcErr);
}
}
});
} catch (err) {
res.status(404).json({ error: 'File not found' });
}
});
// router.get('/download/:filename', async (req, res) => {
// try {
// const { filename } = req.params;
// const filePath = path.join(outputDir, filename);
// await fs.access(filePath);
// res.download(filePath, filename, async (err) => {
// if (err && err.code !== 'ERR_HTTP_HEADERS_SENT') {
// console.error('Download error:', err);
// } else {
// // Auto-delete after successful download
// try {
// await fs.unlink(filePath);
// console.log(`File deleted: ${filename}`);
// } catch (delErr) {
// console.error('Delete error:', delErr);
// }
// }
// });
// } catch (err) {
// res.status(404).json({ error: 'File not found' });
// }
// });
// Download all files as ZIP
router.post('/download-zip', async (req, res) => {
try {
const { filenames } = req.body;
if (!filenames || !Array.isArray(filenames) || filenames.length === 0) {
return res.status(400).json({ error: 'No files specified' });
}
const zipFilename = `download-${uuidv4()}.zip`;
const zipPath = path.join(outputDir, zipFilename);
const output = createWriteStream(zipPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', async () => {
res.download(zipPath, zipFilename, async (err) => {
if (err && err.code !== 'ERR_HTTP_HEADERS_SENT') {
console.error('Download error:', err);
} else {
// Auto-delete downloaded ZIP and the included output files
try {
for (const filename of filenames) {
const filePath = path.join(outputDir, filename);
try {
await fs.unlink(filePath);
console.log(`Output file deleted: ${filename}`);
} catch (deleteErr) {
console.error(`Could not delete output file ${filename}:`, deleteErr);
}
}
await fs.unlink(zipPath);
console.log(`ZIP file deleted: ${zipFilename}`);
// ALSO clear the source folder so both source + output end up empty
try {
await clearDirectory(sourceDir);
console.log('Source directory cleared after zip download');
} catch (srcErr) {
console.error('Could not clear source directory:', srcErr);
}
} catch (delErr) {
console.error('Delete error:', delErr);
}
}
});
});
archive.on('error', (err) => {
res.status(500).json({ error: err.message });
});
archive.pipe(output);
// Add files to archive
for (const filename of filenames) {
const filePath = path.join(outputDir, filename);
try {
await fs.access(filePath);
archive.file(filePath, { name: filename });
} catch (err) {
console.warn(`File not found: ${filename}`);
}
}
await archive.finalize();
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Delete file
router.delete('/delete/:filename', async (req, res) => {
try {
const { filename } = req.params;
const filePath = path.join(outputDir, filename);
await fs.unlink(filePath);
res.json({ message: 'File deleted successfully' });
} catch (err) {
res.status(500).json({ error: 'File not found' });
}
});
// List output files
router.get('/outputs', async (req, res) => {
try {
const baseUrl = process.env.BASE_URL;
const files = await fs.readdir(outputDir);
const fileDetails = await Promise.all(
files.map(async (file) => {
const filePath = path.join(outputDir, file);
const stats = await fs.stat(filePath);
return {
name: file,
size: stats.size,
url: `${baseUrl}/outputs/${encodeURIComponent(file)}`,
createdAt: stats.birthtime
};
})
);
res.json({ files: fileDetails });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
export default router;
View File
View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB