|
import express from 'express';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import githubService from '../services/githubService.js';
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
router.use((req, res, next) => {
|
|
console.log(`PPT Router - ${req.method} ${req.path} - Body:`, Object.keys(req.body || {}));
|
|
next();
|
|
});
|
|
|
|
|
|
router.get('/list', async (req, res, next) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const pptList = await githubService.getUserPPTList(userId);
|
|
res.json(pptList);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
router.get('/:pptId', async (req, res, next) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const { pptId } = req.params;
|
|
|
|
console.log(`🔍 Fetching PPT: ${pptId} for user: ${userId}`);
|
|
|
|
let pptData = null;
|
|
let foundInRepo = -1;
|
|
|
|
if (githubService.useMemoryStorage) {
|
|
|
|
const result = await githubService.getPPTFromMemory(userId, pptId);
|
|
if (result && result.content) {
|
|
pptData = result.content;
|
|
foundInRepo = 0;
|
|
console.log(`✅ PPT found in memory storage`);
|
|
}
|
|
} else {
|
|
|
|
console.log(`Available repositories: ${githubService.repositories.length}`);
|
|
|
|
for (let i = 0; i < githubService.repositories.length; i++) {
|
|
try {
|
|
console.log(`📂 Checking repository ${i}: ${githubService.repositories[i]}`);
|
|
const result = await githubService.getPPT(userId, pptId, i);
|
|
if (result && result.content) {
|
|
pptData = result.content;
|
|
foundInRepo = i;
|
|
console.log(`✅ PPT found in repository ${i}${result.isReassembled ? ' (reassembled from chunks)' : ''}`);
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.log(`❌ PPT not found in repository ${i}: ${error.message}`);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!pptData) {
|
|
console.log(`❌ PPT ${pptId} not found for user ${userId}`);
|
|
return res.status(404).json({
|
|
error: 'PPT not found',
|
|
pptId: pptId,
|
|
userId: userId,
|
|
storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
|
|
});
|
|
}
|
|
|
|
|
|
const standardizedPptData = {
|
|
|
|
id: pptData.id || pptData.pptId || pptId,
|
|
pptId: pptData.pptId || pptData.id || pptId,
|
|
title: pptData.title || '未命名演示文稿',
|
|
|
|
|
|
slides: Array.isArray(pptData.slides) ? pptData.slides.map((slide, index) => ({
|
|
id: slide.id || `slide-${index}`,
|
|
elements: Array.isArray(slide.elements) ? slide.elements : [],
|
|
background: slide.background || { type: 'solid', color: '#ffffff' },
|
|
...slide
|
|
})) : [],
|
|
|
|
|
|
theme: pptData.theme || {
|
|
backgroundColor: '#ffffff',
|
|
themeColor: '#d14424',
|
|
fontColor: '#333333',
|
|
fontName: 'Microsoft YaHei'
|
|
},
|
|
|
|
|
|
viewportSize: pptData.viewportSize || 1000,
|
|
viewportRatio: pptData.viewportRatio || 0.5625,
|
|
|
|
|
|
createdAt: pptData.createdAt || new Date().toISOString(),
|
|
updatedAt: pptData.updatedAt || new Date().toISOString(),
|
|
|
|
|
|
...pptData
|
|
};
|
|
|
|
|
|
if (standardizedPptData.slides.length === 0) {
|
|
console.log(`⚠️ PPT ${pptId} has no slides, creating default slide`);
|
|
standardizedPptData.slides = [{
|
|
id: 'default-slide',
|
|
elements: [],
|
|
background: { type: 'solid', color: '#ffffff' }
|
|
}];
|
|
}
|
|
|
|
|
|
if (standardizedPptData.viewportRatio <= 0 || standardizedPptData.viewportRatio > 2) {
|
|
console.log(`⚠️ Invalid viewportRatio ${standardizedPptData.viewportRatio}, resetting to 0.5625`);
|
|
standardizedPptData.viewportRatio = 0.5625;
|
|
}
|
|
|
|
|
|
if (standardizedPptData.viewportSize <= 0 || standardizedPptData.viewportSize > 2000) {
|
|
console.log(`⚠️ Invalid viewportSize ${standardizedPptData.viewportSize}, resetting to 1000`);
|
|
standardizedPptData.viewportSize = 1000;
|
|
}
|
|
|
|
console.log(`✅ Successfully found and standardized PPT ${pptId}:`, {
|
|
slidesCount: standardizedPptData.slides.length,
|
|
viewportSize: standardizedPptData.viewportSize,
|
|
viewportRatio: standardizedPptData.viewportRatio,
|
|
storageMode: githubService.useMemoryStorage ? 'memory' : `repository ${foundInRepo}`
|
|
});
|
|
|
|
res.json(standardizedPptData);
|
|
} catch (error) {
|
|
console.error(`❌ Error fetching PPT ${req.params.pptId}:`, error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
router.post('/save', async (req, res, next) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const { pptId, title, slides, theme, viewportSize, viewportRatio } = req.body;
|
|
|
|
console.log(`🔄 Starting PPT save for user ${userId}, PPT ID: ${pptId}`);
|
|
console.log(`📊 Request body analysis:`, {
|
|
pptId: !!pptId,
|
|
title: title?.length || 0,
|
|
slidesCount: Array.isArray(slides) ? slides.length : 'not array',
|
|
theme: !!theme,
|
|
requestSize: req.get('content-length'),
|
|
storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
|
|
});
|
|
|
|
if (!pptId || !slides) {
|
|
console.log(`❌ Missing required fields - pptId: ${!!pptId}, slides: ${!!slides}`);
|
|
return res.status(400).json({ error: 'PPT ID and slides are required' });
|
|
}
|
|
|
|
|
|
if (!Array.isArray(slides) || slides.length === 0) {
|
|
console.log(`❌ Invalid slides array - isArray: ${Array.isArray(slides)}, length: ${slides?.length || 0}`);
|
|
return res.status(400).json({ error: 'Slides must be a non-empty array' });
|
|
}
|
|
|
|
|
|
const pptData = {
|
|
id: pptId,
|
|
title: title || '未命名演示文稿',
|
|
slides: slides.map((slide, index) => ({
|
|
id: slide.id || `slide-${index}`,
|
|
elements: Array.isArray(slide.elements) ? slide.elements : [],
|
|
background: slide.background || { type: 'solid', color: '#ffffff' },
|
|
...slide
|
|
})),
|
|
theme: theme || {
|
|
backgroundColor: '#ffffff',
|
|
themeColor: '#d14424',
|
|
fontColor: '#333333',
|
|
fontName: 'Microsoft YaHei'
|
|
},
|
|
viewportSize: viewportSize || 1000,
|
|
viewportRatio: viewportRatio || 0.5625,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
|
|
const jsonData = JSON.stringify(pptData);
|
|
const totalDataSize = Buffer.byteLength(jsonData, 'utf8');
|
|
|
|
console.log(`📊 PPT data analysis:`);
|
|
console.log(` - Total slides: ${slides.length}`);
|
|
console.log(` - Total data size: ${totalDataSize} bytes (${(totalDataSize / 1024).toFixed(2)} KB)`);
|
|
console.log(` - Average slide size: ${(totalDataSize / slides.length / 1024).toFixed(2)} KB`);
|
|
|
|
|
|
const slideAnalysis = slides.map((slide, index) => {
|
|
const slideJson = JSON.stringify(slide);
|
|
const slideSize = Buffer.byteLength(slideJson, 'utf8');
|
|
return {
|
|
index,
|
|
size: slideSize,
|
|
sizeKB: (slideSize / 1024).toFixed(2),
|
|
elementsCount: slide.elements?.length || 0,
|
|
hasLargeContent: slideSize > 800 * 1024
|
|
};
|
|
});
|
|
|
|
const largeSlides = slideAnalysis.filter(s => s.hasLargeContent);
|
|
const maxSlideSize = Math.max(...slideAnalysis.map(s => s.size));
|
|
|
|
console.log(`📋 Slide size analysis:`);
|
|
console.log(` - Largest slide: ${(maxSlideSize / 1024).toFixed(2)} KB`);
|
|
console.log(` - Large slides (>800KB): ${largeSlides.length}`);
|
|
if (largeSlides.length > 0) {
|
|
console.log(` - Large slide indices: ${largeSlides.map(s => s.index).join(', ')}`);
|
|
}
|
|
|
|
try {
|
|
let result;
|
|
|
|
console.log(`💾 Using storage mode: ${githubService.useMemoryStorage ? 'memory' : 'GitHub folder architecture'}`);
|
|
|
|
if (githubService.useMemoryStorage) {
|
|
|
|
result = await githubService.savePPTToMemory(userId, pptId, pptData);
|
|
console.log(`✅ PPT saved to memory storage:`, result);
|
|
} else {
|
|
|
|
console.log(`🐙 Using GitHub folder storage for ${pptId}`);
|
|
console.log(`📂 Available repositories: ${githubService.repositories?.length || 0}`);
|
|
|
|
if (!githubService.repositories || githubService.repositories.length === 0) {
|
|
throw new Error('No GitHub repositories configured');
|
|
}
|
|
|
|
console.log(`🔍 Pre-save validation:`);
|
|
console.log(` - Repository 0: ${githubService.repositories[0]}`);
|
|
console.log(` - Token configured: ${!!githubService.token}`);
|
|
console.log(` - API URL: ${githubService.apiUrl}`);
|
|
|
|
result = await githubService.savePPT(userId, pptId, pptData, 0);
|
|
console.log(`✅ PPT saved to GitHub folder storage:`, result);
|
|
}
|
|
|
|
console.log(`✅ PPT save completed successfully:`, {
|
|
pptId,
|
|
userId,
|
|
slideCount: slides.length,
|
|
totalDataSize,
|
|
storageType: result.storage || 'unknown',
|
|
compressionApplied: result.compressionSummary?.compressedSlides > 0,
|
|
storageMode: githubService.useMemoryStorage ? 'memory' : 'github'
|
|
});
|
|
|
|
const response = {
|
|
message: 'PPT saved successfully',
|
|
pptId,
|
|
slidesCount: slides.length,
|
|
savedAt: new Date().toISOString(),
|
|
totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`,
|
|
storageType: result.storage || 'unknown',
|
|
storageMode: githubService.useMemoryStorage ? 'memory' : 'github',
|
|
architecture: 'folder-based',
|
|
slideAnalysis: {
|
|
totalSlides: slides.length,
|
|
largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
|
|
largeSlides: largeSlides.length,
|
|
averageSlideSize: `${(totalDataSize / slides.length / 1024).toFixed(2)} KB`
|
|
}
|
|
};
|
|
|
|
|
|
if (result.compressionSummary) {
|
|
response.compression = {
|
|
applied: result.compressionSummary.compressedSlides > 0,
|
|
compressedSlides: result.compressionSummary.compressedSlides,
|
|
totalSlides: result.compressionSummary.totalSlides,
|
|
savedSlides: result.compressionSummary.savedSlides,
|
|
failedSlides: result.compressionSummary.failedSlides,
|
|
originalSize: `${(result.compressionSummary.totalOriginalSize / 1024).toFixed(2)} KB`,
|
|
finalSize: `${(result.compressionSummary.totalFinalSize / 1024).toFixed(2)} KB`,
|
|
savedSpace: `${((result.compressionSummary.totalOriginalSize - result.compressionSummary.totalFinalSize) / 1024).toFixed(2)} KB`,
|
|
compressionRatio: `${(result.compressionSummary.compressionRatio * 100).toFixed(1)}%`
|
|
};
|
|
|
|
if (result.compressionSummary.compressedSlides > 0) {
|
|
response.message = `PPT saved successfully with ${result.compressionSummary.compressedSlides} slides optimized for storage`;
|
|
response.optimization = `Automatic compression applied to ${result.compressionSummary.compressedSlides} large slides, saving ${response.compression.savedSpace}`;
|
|
}
|
|
|
|
|
|
if (result.compressionSummary.failedSlides > 0) {
|
|
response.warning = `${result.compressionSummary.failedSlides} slides failed to save`;
|
|
response.partialSave = true;
|
|
response.savedSlides = result.compressionSummary.savedSlides;
|
|
response.failedSlides = result.compressionSummary.failedSlides;
|
|
|
|
if (result.compressionSummary.errors) {
|
|
response.slideErrors = result.compressionSummary.errors;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (result.folderPath) {
|
|
response.storagePath = result.folderPath;
|
|
response.architecture = 'folder-based (meta.json + individual slide files)';
|
|
}
|
|
|
|
|
|
if (result.warnings) {
|
|
response.warnings = result.warnings;
|
|
}
|
|
|
|
res.json(response);
|
|
|
|
} catch (saveError) {
|
|
console.error(`❌ Save operation failed:`, {
|
|
error: saveError.message,
|
|
stack: saveError.stack,
|
|
userId,
|
|
pptId,
|
|
slidesCount: slides.length,
|
|
totalDataSize,
|
|
errorName: saveError.name,
|
|
errorCode: saveError.code
|
|
});
|
|
|
|
let errorResponse = {
|
|
error: 'PPT save failed',
|
|
details: saveError.message,
|
|
pptId: pptId,
|
|
slidesCount: slides.length,
|
|
totalFileSize: `${(totalDataSize / 1024).toFixed(2)} KB`,
|
|
timestamp: new Date().toISOString(),
|
|
slideAnalysis: {
|
|
largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
|
|
largeSlides: largeSlides.length
|
|
}
|
|
};
|
|
|
|
|
|
if (saveError.message.includes('File too large') || saveError.message.includes('exceeds')) {
|
|
errorResponse.error = 'Slide file too large for storage';
|
|
errorResponse.suggestion = 'Some slides are too large even after compression. Try reducing image sizes or slide complexity.';
|
|
errorResponse.limits = {
|
|
maxSlideSize: '999 KB per slide',
|
|
largestSlideSize: `${(maxSlideSize / 1024).toFixed(2)} KB`,
|
|
oversizeSlides: largeSlides.length
|
|
};
|
|
return res.status(413).json(errorResponse);
|
|
}
|
|
|
|
if (saveError.message.includes('Failed to compress slide')) {
|
|
errorResponse.error = 'Slide compression failed';
|
|
errorResponse.suggestion = 'Unable to compress slides to acceptable size. Try manually reducing content complexity.';
|
|
errorResponse.compressionError = true;
|
|
return res.status(500).json(errorResponse);
|
|
}
|
|
|
|
if (saveError.message.includes('GitHub API') || saveError.message.includes('GitHub')) {
|
|
errorResponse.error = 'GitHub storage service error';
|
|
errorResponse.suggestion = 'Temporary GitHub service issue. Please try again in a few minutes.';
|
|
errorResponse.githubError = true;
|
|
return res.status(502).json(errorResponse);
|
|
}
|
|
|
|
if (saveError.message.includes('No GitHub repositories')) {
|
|
errorResponse.error = 'Storage configuration error';
|
|
errorResponse.suggestion = 'GitHub repositories not properly configured. Please contact support.';
|
|
errorResponse.configError = true;
|
|
return res.status(500).json(errorResponse);
|
|
}
|
|
|
|
|
|
if (saveError.code === 'ETIMEDOUT' || saveError.message.includes('timeout')) {
|
|
errorResponse.error = 'Storage operation timeout';
|
|
errorResponse.suggestion = 'The save operation took too long. Try reducing file size or try again later.';
|
|
errorResponse.timeoutError = true;
|
|
return res.status(504).json(errorResponse);
|
|
}
|
|
|
|
|
|
errorResponse.suggestion = 'Unknown save error. Please try again or contact support if the problem persists.';
|
|
errorResponse.unknownError = true;
|
|
return res.status(500).json(errorResponse);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`❌ PPT save failed for user ${req.user?.userId}, PPT ID: ${req.body?.pptId}:`, {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
userId: req.user?.userId,
|
|
pptId: req.body?.pptId,
|
|
requestSize: req.get('content-length'),
|
|
slidesCount: req.body?.slides?.length,
|
|
errorName: error.name,
|
|
errorCode: error.code
|
|
});
|
|
|
|
|
|
let errorResponse = {
|
|
error: 'PPT save processing failed',
|
|
details: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
userId: req.user?.userId,
|
|
pptId: req.body?.pptId,
|
|
architecture: 'folder-based'
|
|
};
|
|
|
|
if (error.message.includes('Invalid slide data')) {
|
|
errorResponse.error = 'Invalid slide data detected';
|
|
errorResponse.suggestion = 'One or more slides contain invalid or corrupted data';
|
|
return res.status(400).json(errorResponse);
|
|
}
|
|
|
|
if (error.message.includes('JSON')) {
|
|
errorResponse.error = 'Invalid data format';
|
|
errorResponse.suggestion = 'Check slide data structure and content';
|
|
return res.status(400).json(errorResponse);
|
|
}
|
|
|
|
errorResponse.suggestion = 'Unknown processing error. Please try again or contact support if the problem persists.';
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
router.post('/create', async (req, res, next) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const { title } = req.body;
|
|
|
|
if (!title) {
|
|
return res.status(400).json({ error: 'Title is required' });
|
|
}
|
|
|
|
const pptId = uuidv4();
|
|
const now = new Date().toISOString();
|
|
|
|
|
|
const pptData = {
|
|
id: pptId,
|
|
title,
|
|
theme: {
|
|
backgroundColor: '#ffffff',
|
|
themeColor: '#d14424',
|
|
fontColor: '#333333',
|
|
fontName: 'Microsoft YaHei'
|
|
},
|
|
slides: [
|
|
{
|
|
id: uuidv4(),
|
|
elements: [
|
|
{
|
|
type: 'text',
|
|
id: uuidv4(),
|
|
left: 150,
|
|
top: 200,
|
|
width: 600,
|
|
height: 100,
|
|
content: title,
|
|
fontSize: 32,
|
|
fontName: 'Microsoft YaHei',
|
|
defaultColor: '#333333',
|
|
bold: true,
|
|
align: 'center'
|
|
}
|
|
],
|
|
background: {
|
|
type: 'solid',
|
|
color: '#ffffff'
|
|
}
|
|
}
|
|
],
|
|
|
|
viewportSize: 1000,
|
|
viewportRatio: 0.5625,
|
|
createdAt: now,
|
|
updatedAt: now
|
|
};
|
|
|
|
const fileName = `${pptId}.json`;
|
|
|
|
console.log(`Creating PPT for user ${userId}, using ${githubService.useMemoryStorage ? 'memory' : 'GitHub'} storage`);
|
|
|
|
if (githubService.useMemoryStorage) {
|
|
await githubService.saveToMemory(userId, fileName, pptData);
|
|
} else {
|
|
await githubService.saveFile(userId, fileName, pptData);
|
|
}
|
|
|
|
console.log(`PPT created successfully: ${pptId}`);
|
|
res.json({ success: true, pptId, pptData });
|
|
} catch (error) {
|
|
console.error('PPT creation error:', error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
router.delete('/:pptId', async (req, res, next) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const { pptId } = req.params;
|
|
const fileName = `${pptId}.json`;
|
|
|
|
if (githubService.useMemoryStorage) {
|
|
|
|
const deleted = githubService.memoryStorage.delete(`users/${userId}/${fileName}`);
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: 'PPT not found' });
|
|
}
|
|
} else {
|
|
|
|
let deleted = false;
|
|
for (let i = 0; i < githubService.repositories.length; i++) {
|
|
try {
|
|
await githubService.deleteFile(userId, fileName, i);
|
|
deleted = true;
|
|
break;
|
|
} catch (error) {
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: 'PPT not found in any repository' });
|
|
}
|
|
}
|
|
|
|
res.json({ message: 'PPT deleted successfully' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
|
|
router.post('/:pptId/copy', async (req, res, next) => {
|
|
try {
|
|
const userId = req.user.userId;
|
|
const { pptId } = req.params;
|
|
const { title } = req.body;
|
|
const sourceFileName = `${pptId}.json`;
|
|
|
|
|
|
let sourcePPT = null;
|
|
|
|
if (githubService.useMemoryStorage) {
|
|
|
|
sourcePPT = await githubService.getFromMemory(userId, sourceFileName);
|
|
} else {
|
|
|
|
for (let i = 0; i < githubService.repositories.length; i++) {
|
|
try {
|
|
const result = await githubService.getFile(userId, sourceFileName, i);
|
|
if (result) {
|
|
sourcePPT = result;
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!sourcePPT) {
|
|
return res.status(404).json({ error: 'Source PPT not found' });
|
|
}
|
|
|
|
|
|
const newPptId = uuidv4();
|
|
const newFileName = `${newPptId}.json`;
|
|
const newPPTData = {
|
|
...sourcePPT,
|
|
id: newPptId,
|
|
title: title || `${sourcePPT.title} - 副本`,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
|
|
if (githubService.useMemoryStorage) {
|
|
await githubService.saveToMemory(userId, newFileName, newPPTData);
|
|
} else {
|
|
await githubService.saveFile(userId, newFileName, newPPTData, 0);
|
|
}
|
|
|
|
res.json({
|
|
message: 'PPT copied successfully',
|
|
pptId: newPptId,
|
|
ppt: newPPTData
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router; |