|
import axios from 'axios';
|
|
|
|
class GitHubService {
|
|
constructor() {
|
|
|
|
this.initialized = false;
|
|
this.useMemoryStorage = false;
|
|
this.memoryStorage = new Map();
|
|
this.apiUrl = 'https://api.github.com';
|
|
|
|
|
|
this.initPromise = null;
|
|
|
|
|
|
this.retryConfig = {
|
|
maxRetries: 3,
|
|
retryDelay: 1000,
|
|
backoffMultiplier: 2
|
|
};
|
|
|
|
|
|
this.apiRateLimit = {
|
|
requestQueue: [],
|
|
processing: false,
|
|
minDelay: 100
|
|
};
|
|
}
|
|
|
|
|
|
async initialize() {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
|
|
console.log('=== GitHub Service Configuration ===');
|
|
|
|
try {
|
|
|
|
this.token = process.env.GITHUB_TOKEN;
|
|
this.repositories = process.env.GITHUB_REPOS
|
|
? process.env.GITHUB_REPOS.split(',').map(repo => repo.trim())
|
|
: [];
|
|
|
|
console.log('Token configured:', !!this.token);
|
|
console.log('Token preview:', this.token ? `${this.token.substring(0, 8)}...` : 'Not set');
|
|
console.log('Repositories:', this.repositories);
|
|
console.log('Repositories length:', this.repositories?.length || 0);
|
|
|
|
|
|
if (!this.token || this.token === 'your_github_token_here') {
|
|
console.warn('❌ GitHub token is missing or using placeholder value!');
|
|
console.warn('⚠️ Switching to memory storage mode for development...');
|
|
this.useMemoryStorage = true;
|
|
console.log('✅ Memory storage mode activated');
|
|
this.initialized = true;
|
|
return;
|
|
}
|
|
|
|
if (!this.repositories || this.repositories.length === 0) {
|
|
console.warn('❌ No GitHub repositories configured!');
|
|
console.warn('⚠️ Switching to memory storage mode...');
|
|
this.useMemoryStorage = true;
|
|
console.log('✅ Memory storage mode activated');
|
|
this.initialized = true;
|
|
return;
|
|
}
|
|
|
|
|
|
const validRepos = this.repositories.filter(repo => {
|
|
const isPlaceholder = repo.includes('your-username') || repo.includes('placeholder');
|
|
if (isPlaceholder) {
|
|
console.warn(`⚠️ Skipping placeholder repository: ${repo}`);
|
|
return false;
|
|
}
|
|
return this.isValidRepoUrl(repo);
|
|
});
|
|
|
|
if (validRepos.length === 0) {
|
|
console.warn('❌ No valid GitHub repositories found (all are placeholders)!');
|
|
console.warn('⚠️ Switching to memory storage mode...');
|
|
this.useMemoryStorage = true;
|
|
console.log('✅ Memory storage mode activated');
|
|
this.initialized = true;
|
|
return;
|
|
}
|
|
|
|
this.repositories = validRepos;
|
|
|
|
|
|
this.repositories.forEach((repo, index) => {
|
|
if (!this.isValidRepoUrl(repo)) {
|
|
console.error(`❌ Invalid repository URL at index ${index}: ${repo}`);
|
|
throw new Error(`Invalid repository URL: ${repo}. Expected format: https://github.com/owner/repo`);
|
|
}
|
|
});
|
|
|
|
console.log('✅ GitHub Service initialized successfully');
|
|
this.initialized = true;
|
|
} catch (error) {
|
|
console.error('❌ GitHub Service initialization failed:', error);
|
|
console.warn('⚠️ Falling back to memory storage mode...');
|
|
this.useMemoryStorage = true;
|
|
this.initialized = true;
|
|
}
|
|
}
|
|
|
|
|
|
async ensureInitialized() {
|
|
if (!this.initPromise) {
|
|
this.initPromise = this.initialize();
|
|
}
|
|
await this.initPromise;
|
|
}
|
|
|
|
|
|
isValidRepoUrl(repoUrl) {
|
|
const githubUrlPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
|
|
return githubUrlPattern.test(repoUrl.trim());
|
|
}
|
|
|
|
|
|
async makeGitHubRequest(requestFn, operation = 'GitHub API request', maxRetries = null) {
|
|
const retries = maxRetries || this.retryConfig.maxRetries;
|
|
let lastError = null;
|
|
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
try {
|
|
if (attempt > 0) {
|
|
const delay = this.retryConfig.retryDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt - 1);
|
|
console.log(`🔄 Retrying ${operation} (attempt ${attempt + 1}/${retries + 1}) after ${delay}ms...`);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
|
|
const result = await requestFn();
|
|
|
|
if (attempt > 0) {
|
|
console.log(`✅ ${operation} succeeded on retry attempt ${attempt + 1}`);
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
|
|
if (!this.shouldRetry(error) || attempt === retries) {
|
|
break;
|
|
}
|
|
|
|
console.warn(`⚠️ ${operation} failed (attempt ${attempt + 1}):`, error.message);
|
|
}
|
|
}
|
|
|
|
console.error(`❌ ${operation} failed after ${retries + 1} attempts:`, lastError.message);
|
|
throw lastError;
|
|
}
|
|
|
|
|
|
shouldRetry(error) {
|
|
if (!error.response) {
|
|
return true;
|
|
}
|
|
|
|
const status = error.response.status;
|
|
|
|
|
|
if ([400, 401, 403, 404, 422].includes(status)) {
|
|
return false;
|
|
}
|
|
|
|
|
|
if (status >= 500 || status === 429) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
async validateConnection() {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
console.log('📝 Memory storage mode active');
|
|
return {
|
|
valid: true,
|
|
useMemoryStorage: true,
|
|
repositories: [],
|
|
message: 'Using memory storage mode for development'
|
|
};
|
|
}
|
|
|
|
try {
|
|
|
|
const response = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(`${this.apiUrl}/user`, {
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
});
|
|
}, 'GitHub user authentication');
|
|
|
|
console.log('GitHub API connection successful:', response.data.login);
|
|
|
|
|
|
const repoResults = [];
|
|
for (const repoUrl of this.repositories) {
|
|
try {
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
|
|
const repoResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(`${this.apiUrl}/repos/${owner}/${repo}`, {
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
});
|
|
}, `Repository access check for ${owner}/${repo}`);
|
|
|
|
|
|
const permissions = repoResponse.data.permissions;
|
|
console.log(`Repository permissions for ${owner}/${repo}:`, permissions);
|
|
|
|
|
|
let canAccessContents = false;
|
|
let repoEmpty = false;
|
|
try {
|
|
await axios.get(`${this.apiUrl}/repos/${owner}/${repo}/contents`, {
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 15000
|
|
});
|
|
canAccessContents = true;
|
|
} catch (contentsError) {
|
|
console.log(`Cannot access repository contents: ${contentsError.message}`);
|
|
|
|
|
|
if (contentsError.response?.status === 409) {
|
|
repoEmpty = true;
|
|
console.log('Repository appears to be empty, will initialize when needed');
|
|
}
|
|
}
|
|
|
|
repoResults.push({
|
|
url: repoUrl,
|
|
accessible: true,
|
|
name: repoResponse.data.full_name,
|
|
permissions,
|
|
canAccessContents,
|
|
repoEmpty,
|
|
defaultBranch: repoResponse.data.default_branch,
|
|
size: repoResponse.data.size
|
|
});
|
|
} catch (error) {
|
|
console.error(`Repository access error for ${repoUrl}:`, error.response?.data || error.message);
|
|
repoResults.push({ url: repoUrl, accessible: false, error: error.message });
|
|
}
|
|
}
|
|
|
|
return { valid: true, user: response.data.login, repositories: repoResults };
|
|
} catch (error) {
|
|
console.error('GitHub connection validation failed:', error.message);
|
|
return { valid: false, reason: error.message };
|
|
}
|
|
}
|
|
|
|
|
|
parseRepoUrl(repoUrl) {
|
|
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
if (!match) throw new Error('Invalid GitHub repository URL');
|
|
return { owner: match[1], repo: match[2] };
|
|
}
|
|
|
|
|
|
async initializeRepository(repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
console.log('📝 Memory storage mode - no repository initialization needed');
|
|
return { success: true, message: 'Memory storage initialized' };
|
|
}
|
|
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
try {
|
|
console.log(`🚀 Initializing repository: ${owner}/${repo}`);
|
|
|
|
|
|
const readmeContent = `# PPT Storage Repository\n\nThis repository is used to store PPT data files.\n\nCreated: ${new Date().toISOString()}`;
|
|
const content = Buffer.from(readmeContent).toString('base64');
|
|
|
|
await this.makeGitHubRequest(async () => {
|
|
return await axios.put(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/README.md`,
|
|
{
|
|
message: 'Initialize PPT storage repository',
|
|
content: content
|
|
},
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Repository initialization for ${owner}/${repo}`);
|
|
|
|
console.log(`✅ Repository ${owner}/${repo} initialized successfully`);
|
|
return { success: true, message: 'Repository initialized' };
|
|
} catch (error) {
|
|
console.error(`❌ Repository initialization failed:`, error);
|
|
throw new Error(`Failed to initialize repository: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
async getFile(userId, fileName, repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
const pptId = fileName.replace('.json', '');
|
|
return await this.getPPT(userId, pptId, repoIndex);
|
|
}
|
|
|
|
|
|
async saveFile(userId, fileName, data, repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
const pptId = fileName.replace('.json', '');
|
|
return await this.savePPT(userId, pptId, data, repoIndex);
|
|
}
|
|
|
|
|
|
normalizeDataFormat(data) {
|
|
if (!data || typeof data !== 'object') {
|
|
throw new Error('Invalid data format provided');
|
|
}
|
|
|
|
const normalized = {
|
|
id: data.id || data.pptId || `ppt-${Date.now()}`,
|
|
title: data.title || '未命名演示文稿',
|
|
slides: Array.isArray(data.slides) ? data.slides : [],
|
|
theme: data.theme || {
|
|
backgroundColor: '#ffffff',
|
|
themeColor: '#d14424',
|
|
fontColor: '#333333',
|
|
fontName: 'Microsoft YaHei'
|
|
},
|
|
viewportSize: data.viewportSize || 1000,
|
|
viewportRatio: data.viewportRatio || 0.5625,
|
|
createdAt: data.createdAt || new Date().toISOString(),
|
|
updatedAt: new Date().toISOString()
|
|
};
|
|
|
|
|
|
normalized.slides = normalized.slides.map((slide, index) => {
|
|
if (!slide || typeof slide !== 'object') {
|
|
return {
|
|
id: `slide-${index}`,
|
|
elements: [],
|
|
background: { type: 'solid', color: '#ffffff' }
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: slide.id || `slide-${index}`,
|
|
elements: Array.isArray(slide.elements) ? slide.elements : [],
|
|
background: slide.background || { type: 'solid', color: '#ffffff' },
|
|
...slide
|
|
};
|
|
});
|
|
|
|
|
|
Object.keys(data).forEach(key => {
|
|
if (!normalized.hasOwnProperty(key) && key !== 'slides') {
|
|
normalized[key] = data[key];
|
|
}
|
|
});
|
|
|
|
return normalized;
|
|
}
|
|
|
|
|
|
async deleteFile(userId, fileName, repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
const key = `users/${userId}/${fileName}`;
|
|
return this.memoryStorage.delete(key);
|
|
}
|
|
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
const path = `users/${userId}/${fileName}`;
|
|
|
|
try {
|
|
|
|
const response = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Get file info for deletion: ${fileName}`);
|
|
|
|
|
|
await this.makeGitHubRequest(async () => {
|
|
return await axios.delete(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
|
|
{
|
|
data: {
|
|
message: `Delete legacy PPT: ${fileName}`,
|
|
sha: response.data.sha
|
|
},
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Delete legacy file: ${fileName}`);
|
|
|
|
console.log(`✅ Legacy file deleted: ${fileName}`);
|
|
return true;
|
|
} catch (error) {
|
|
if (error.response?.status === 404) {
|
|
console.log(`📄 File not found: ${fileName}`);
|
|
return false;
|
|
}
|
|
console.error(`❌ Delete failed for ${fileName}:`, error);
|
|
throw new Error(`Failed to delete file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
async saveToMemory(userId, fileName, data) {
|
|
const pptId = fileName.replace('.json', '');
|
|
return await this.savePPTToMemory(userId, pptId, data);
|
|
}
|
|
|
|
async getFromMemory(userId, fileName) {
|
|
const pptId = fileName.replace('.json', '');
|
|
const result = await this.getPPTFromMemory(userId, pptId);
|
|
return result ? result.content : null;
|
|
}
|
|
|
|
|
|
async getPPT(userId, pptId, repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
return await this.getPPTFromMemory(userId, pptId);
|
|
}
|
|
|
|
try {
|
|
console.log(`📂 Getting PPT folder: ${pptId} for user: ${userId}`);
|
|
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
const pptFolderPath = `users/${userId}/${pptId}`;
|
|
|
|
|
|
const folderResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${pptFolderPath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Get PPT folder contents for ${pptId}`);
|
|
|
|
|
|
const metaFile = folderResponse.data.find(file => file.name === 'meta.json');
|
|
if (!metaFile) {
|
|
throw new Error('PPT metadata file not found');
|
|
}
|
|
|
|
const metaResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${metaFile.path}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 15000
|
|
}
|
|
);
|
|
}, `Get PPT metadata for ${pptId}`);
|
|
|
|
let metadata;
|
|
try {
|
|
const metaContent = Buffer.from(metaResponse.data.content, 'base64').toString('utf8');
|
|
metadata = JSON.parse(metaContent);
|
|
} catch (parseError) {
|
|
console.error('❌ Failed to parse metadata:', parseError);
|
|
throw new Error('Invalid PPT metadata format');
|
|
}
|
|
|
|
|
|
const allFiles = folderResponse.data;
|
|
const slides = [];
|
|
const failedSlides = [];
|
|
|
|
|
|
const slideFiles = allFiles.filter(file =>
|
|
file.name.startsWith('slide_') &&
|
|
file.name.endsWith('.json') &&
|
|
!file.name.includes('_main') &&
|
|
!file.name.includes('_part_')
|
|
);
|
|
|
|
|
|
const sortedSlideFiles = slideFiles.sort((a, b) => {
|
|
const aIndex = parseInt(a.name.match(/slide_(\d+)\.json/)?.[1] || '0');
|
|
const bIndex = parseInt(b.name.match(/slide_(\d+)\.json/)?.[1] || '0');
|
|
return aIndex - bIndex;
|
|
});
|
|
|
|
for (const slideFile of sortedSlideFiles) {
|
|
try {
|
|
const slide = await this.loadSlideFile(slideFile, repoIndex);
|
|
const slideIndex = parseInt(slideFile.name.match(/slide_(\d+)\.json/)?.[1] || '0');
|
|
slides[slideIndex] = slide;
|
|
} catch (slideError) {
|
|
console.warn(`⚠️ Failed to load slide ${slideFile.name}:`, slideError.message);
|
|
failedSlides.push(slideFile.name);
|
|
}
|
|
}
|
|
|
|
|
|
const finalSlides = slides.filter(slide => slide !== undefined);
|
|
const pptData = {
|
|
...metadata,
|
|
slides: finalSlides,
|
|
storage: {
|
|
type: 'folder',
|
|
slidesCount: finalSlides.length,
|
|
folderPath: pptFolderPath,
|
|
loadedAt: new Date().toISOString(),
|
|
failedSlides: failedSlides.length > 0 ? failedSlides : undefined
|
|
}
|
|
};
|
|
|
|
if (failedSlides.length > 0) {
|
|
console.warn(`⚠️ PPT loaded with ${failedSlides.length} failed slides`);
|
|
} else {
|
|
console.log(`✅ PPT loaded successfully: ${finalSlides.length} slides`);
|
|
}
|
|
|
|
return { content: pptData };
|
|
|
|
} catch (error) {
|
|
if (error.response?.status === 404) {
|
|
console.log(`📄 PPT folder not found: ${pptId}`);
|
|
return null;
|
|
}
|
|
console.error(`❌ Get PPT failed:`, error);
|
|
throw new Error(`Failed to get PPT: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
async checkFileSize(filePath, repoIndex) {
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
try {
|
|
const response = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 15000
|
|
}
|
|
);
|
|
}, `Check file size for ${filePath}`);
|
|
|
|
return {
|
|
size: response.data.size,
|
|
sha: response.data.sha
|
|
};
|
|
} catch (error) {
|
|
if (error.response?.status === 404) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
async readFileContent(filePath, repoIndex, maxDirectSize = 1024 * 1024) {
|
|
const fileInfo = await this.checkFileSize(filePath, repoIndex);
|
|
|
|
if (!fileInfo) {
|
|
return null;
|
|
}
|
|
|
|
|
|
if (fileInfo.size <= maxDirectSize) {
|
|
return await this.readFileContentDirect(filePath, repoIndex);
|
|
}
|
|
|
|
|
|
console.log(`📊 Large file detected (${(fileInfo.size / 1024 / 1024).toFixed(2)} MB), using blob API`);
|
|
return await this.readFileContentViaBlob(filePath, fileInfo.sha, repoIndex);
|
|
}
|
|
|
|
|
|
async readFileContentDirect(filePath, repoIndex) {
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
const response = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Read file content directly: ${filePath}`);
|
|
|
|
const content = Buffer.from(response.data.content, 'base64').toString('utf8');
|
|
return JSON.parse(content);
|
|
}
|
|
|
|
|
|
async readFileContentViaBlob(filePath, sha, repoIndex) {
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
try {
|
|
console.log(`🔍 Reading large file via blob API: ${filePath}`);
|
|
|
|
const response = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/git/blobs/${sha}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 120000
|
|
}
|
|
);
|
|
}, `Read blob for large file: ${filePath}`);
|
|
|
|
if (response.data.encoding === 'base64') {
|
|
const content = Buffer.from(response.data.content, 'base64').toString('utf8');
|
|
return JSON.parse(content);
|
|
} else {
|
|
|
|
return JSON.parse(response.data.content);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ Blob API read failed for ${filePath}:`, error.message);
|
|
|
|
|
|
console.log(`🔄 Falling back to chunked reading...`);
|
|
return await this.readLargeFileInChunks(filePath, repoIndex);
|
|
}
|
|
}
|
|
|
|
|
|
async readLargeFileInChunks(filePath, repoIndex) {
|
|
console.log(`📦 Attempting chunked read for: ${filePath}`);
|
|
|
|
|
|
const folderPath = filePath.replace(/\/[^\/]+$/, '');
|
|
|
|
try {
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
|
|
const folderResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${folderPath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Read folder for chunked file: ${folderPath}`);
|
|
|
|
|
|
const chunkFiles = folderResponse.data.filter(file =>
|
|
file.name.includes('_part_') || file.name.includes('_chunk_')
|
|
);
|
|
|
|
if (chunkFiles.length > 0) {
|
|
console.log(`📊 Found ${chunkFiles.length} chunk files, reassembling...`);
|
|
return await this.reassembleChunkedFiles(chunkFiles, repoIndex);
|
|
}
|
|
|
|
throw new Error('No chunked files found, cannot read large file');
|
|
} catch (error) {
|
|
console.error(`❌ Chunked read failed:`, error.message);
|
|
throw new Error(`Unable to read large file: ${filePath}. File may be too large for current GitHub API limits.`);
|
|
}
|
|
}
|
|
|
|
|
|
async reassembleChunkedFiles(chunkFiles, repoIndex) {
|
|
const chunks = [];
|
|
|
|
|
|
const sortedChunks = chunkFiles.sort((a, b) => {
|
|
const aIndex = parseInt(a.name.match(/_(\d+)(?:_|\.)/)?.[1] || '0');
|
|
const bIndex = parseInt(b.name.match(/_(\d+)(?:_|\.)/)?.[1] || '0');
|
|
return aIndex - bIndex;
|
|
});
|
|
|
|
for (const chunkFile of sortedChunks) {
|
|
try {
|
|
const chunkContent = await this.readFileContentDirect(chunkFile.path, repoIndex);
|
|
chunks.push(chunkContent);
|
|
} catch (error) {
|
|
console.error(`⚠️ Failed to read chunk ${chunkFile.name}:`, error.message);
|
|
}
|
|
}
|
|
|
|
|
|
if (chunks.length === 0) {
|
|
throw new Error('No valid chunks found');
|
|
}
|
|
|
|
|
|
const baseData = chunks[0];
|
|
const allSlides = [];
|
|
|
|
chunks.forEach(chunk => {
|
|
if (chunk.slides && Array.isArray(chunk.slides)) {
|
|
allSlides.push(...chunk.slides);
|
|
}
|
|
});
|
|
|
|
return {
|
|
...baseData,
|
|
slides: allSlides,
|
|
storage: {
|
|
type: 'reassembled',
|
|
chunksCount: chunks.length,
|
|
reassembledAt: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
async loadSlideFile(slideFile, repoIndex) {
|
|
try {
|
|
|
|
const slideContent = await this.readFileContent(slideFile.path, repoIndex);
|
|
return slideContent;
|
|
} catch (error) {
|
|
console.error(`❌ Failed to load slide file ${slideFile.name}:`, error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
async compressSlide(slide) {
|
|
let compressedSlide = JSON.parse(JSON.stringify(slide));
|
|
|
|
|
|
if (compressedSlide.elements) {
|
|
compressedSlide.elements = compressedSlide.elements.map(element => {
|
|
|
|
element = this.removeUnnecessaryProps(element);
|
|
|
|
|
|
if (element.type === 'text' && element.content && typeof element.content === 'string') {
|
|
element.content = element.content.replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
return element;
|
|
});
|
|
}
|
|
|
|
|
|
compressedSlide = this.roundNumericValues(compressedSlide);
|
|
|
|
const compressedSize = Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8');
|
|
console.log(`🗜️ Light compression applied: ${(compressedSize / 1024).toFixed(2)} KB`);
|
|
|
|
return compressedSlide;
|
|
}
|
|
|
|
|
|
removeUnnecessaryProps(element) {
|
|
const unnecessaryProps = [
|
|
'selected', 'editing', 'dragData', 'resizeData',
|
|
'tempData', 'cache', 'debug'
|
|
];
|
|
|
|
unnecessaryProps.forEach(prop => {
|
|
if (element[prop] !== undefined) {
|
|
delete element[prop];
|
|
}
|
|
});
|
|
|
|
return element;
|
|
}
|
|
|
|
|
|
roundNumericValues(obj, precision = 2) {
|
|
if (typeof obj !== 'object' || obj === null) {
|
|
return obj;
|
|
}
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => this.roundNumericValues(item, precision));
|
|
}
|
|
|
|
const rounded = {};
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (typeof value === 'number') {
|
|
|
|
if (['left', 'top', 'width', 'height', 'x', 'y', 'viewportRatio', 'viewportSize'].includes(key)) {
|
|
rounded[key] = Math.round(value * 1000000000) / 1000000000;
|
|
}
|
|
|
|
else if (key.includes('transform') || key.includes('rotate') || key === 'rotate') {
|
|
rounded[key] = Math.round(value * 1000000000) / 1000000000;
|
|
}
|
|
|
|
else {
|
|
rounded[key] = typeof value === 'number' && !isNaN(value) && isFinite(value)
|
|
? Math.round(value * 1000000) / 1000000
|
|
: value;
|
|
}
|
|
} else if (typeof value === 'object') {
|
|
rounded[key] = this.roundNumericValues(value, precision);
|
|
} else {
|
|
rounded[key] = value;
|
|
}
|
|
}
|
|
|
|
return rounded;
|
|
}
|
|
|
|
|
|
async saveFileToRepo(filePath, data, commitMessage, repoIndex) {
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
|
|
try {
|
|
|
|
if (!data || typeof data !== 'object') {
|
|
throw new Error('Invalid data provided for file save');
|
|
}
|
|
|
|
const content = Buffer.from(JSON.stringify(data)).toString('base64');
|
|
const fileSize = Buffer.byteLength(JSON.stringify(data), 'utf8');
|
|
|
|
|
|
if (fileSize > 100 * 1024 * 1024) {
|
|
throw new Error(`File too large: ${(fileSize / 1024 / 1024).toFixed(2)} MB exceeds GitHub's 100MB limit`);
|
|
}
|
|
|
|
console.log(`💾 Saving file: ${filePath} (${(fileSize / 1024).toFixed(2)} KB)`);
|
|
|
|
|
|
let sha = null;
|
|
try {
|
|
const existingResponse = await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
sha = existingResponse.data.sha;
|
|
} catch (error) {
|
|
|
|
}
|
|
|
|
|
|
const response = await axios.put(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${filePath}`,
|
|
{
|
|
message: commitMessage,
|
|
content: content,
|
|
...(sha && { sha })
|
|
},
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 60000
|
|
}
|
|
);
|
|
|
|
console.log(`✅ File saved successfully: ${filePath}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error(`❌ File save failed for ${filePath}:`, error.message);
|
|
|
|
if (error.response?.status === 422) {
|
|
if (error.response?.data?.message?.includes('file is too large')) {
|
|
throw new Error(`File too large: ${filePath} exceeds GitHub size limits`);
|
|
}
|
|
if (error.response?.data?.message?.includes('Invalid request')) {
|
|
throw new Error(`Invalid file format for: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
if (error.response?.status === 409) {
|
|
throw new Error(`File conflict for: ${filePath}. File may have been modified by another process.`);
|
|
}
|
|
|
|
throw new Error(`Failed to save file ${filePath}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
async saveSlideWithCompression(filePath, slide, slideIndex, repoIndex) {
|
|
try {
|
|
|
|
const compressedSlide = await this.compressSlide(slide);
|
|
|
|
await this.makeGitHubRequest(async () => {
|
|
return await this.saveFileToRepo(
|
|
filePath,
|
|
compressedSlide,
|
|
`Save slide ${slideIndex}`,
|
|
repoIndex
|
|
);
|
|
}, `Save slide ${slideIndex}`);
|
|
|
|
const finalSize = Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8');
|
|
|
|
return {
|
|
slideIndex,
|
|
finalSize,
|
|
compressed: true
|
|
};
|
|
} catch (error) {
|
|
console.error(`❌ Failed to save slide ${slideIndex}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
async savePPT(userId, pptId, pptData, repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
return await this.savePPTToMemory(userId, pptId, pptData);
|
|
}
|
|
|
|
try {
|
|
console.log(`📂 Saving PPT to folder: ${pptId} for user: ${userId}`);
|
|
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
const pptFolderPath = `users/${userId}/${pptId}`;
|
|
|
|
|
|
if (!pptData || typeof pptData !== 'object') {
|
|
throw new Error('Invalid PPT data provided');
|
|
}
|
|
|
|
|
|
const metadata = {
|
|
id: pptData.id || pptId,
|
|
title: pptData.title || '未命名演示文稿',
|
|
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: new Date().toISOString(),
|
|
storage: {
|
|
type: 'folder',
|
|
version: '2.0',
|
|
slidesCount: pptData.slides?.length || 0
|
|
}
|
|
};
|
|
|
|
|
|
await this.makeGitHubRequest(async () => {
|
|
return await this.saveFileToRepo(
|
|
`${pptFolderPath}/meta.json`,
|
|
metadata,
|
|
`Update PPT metadata: ${metadata.title}`,
|
|
repoIndex
|
|
);
|
|
}, `Save metadata for PPT ${pptId}`);
|
|
|
|
console.log(`✅ Metadata saved for PPT: ${pptId}`);
|
|
|
|
|
|
const slides = Array.isArray(pptData.slides) ? pptData.slides : [];
|
|
const saveResults = [];
|
|
|
|
console.log(`📊 Starting to save ${slides.length} slides...`);
|
|
|
|
for (let i = 0; i < slides.length; i++) {
|
|
const slide = slides[i];
|
|
const slideFileName = `slide_${String(i).padStart(3, '0')}.json`;
|
|
|
|
console.log(`💾 Saving slide ${i + 1}/${slides.length}: ${slideFileName}`);
|
|
|
|
try {
|
|
|
|
if (!slide || typeof slide !== 'object') {
|
|
throw new Error(`Invalid slide data at index ${i}`);
|
|
}
|
|
|
|
const result = await this.saveSlideWithCompression(
|
|
`${pptFolderPath}/${slideFileName}`,
|
|
slide,
|
|
i,
|
|
repoIndex
|
|
);
|
|
|
|
saveResults.push(result);
|
|
console.log(`✅ Slide ${i} saved: ${(result.finalSize / 1024).toFixed(2)} KB`);
|
|
|
|
|
|
if (i < slides.length - 1) {
|
|
await new Promise(resolve => setTimeout(resolve, this.apiRateLimit.minDelay));
|
|
}
|
|
} catch (slideError) {
|
|
console.error(`❌ Failed to save slide ${i}: ${slideError.message}`);
|
|
throw slideError;
|
|
}
|
|
}
|
|
|
|
|
|
const totalSize = saveResults.reduce((sum, r) => sum + r.finalSize, 0);
|
|
|
|
console.log(`🎉 PPT saved successfully: ${slides.length} slides, total size: ${(totalSize / 1024).toFixed(2)} KB`);
|
|
|
|
return {
|
|
success: true,
|
|
pptId: pptId,
|
|
storage: 'folder',
|
|
slidesCount: slides.length,
|
|
folderPath: pptFolderPath,
|
|
size: totalSize
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error(`❌ PPT save failed for ${pptId}: ${error.message}`);
|
|
throw new Error(`Failed to save PPT: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
async getPPTFromMemory(userId, pptId) {
|
|
const metaKey = `users/${userId}/${pptId}/meta`;
|
|
const metadata = this.memoryStorage.get(metaKey);
|
|
|
|
if (!metadata) {
|
|
console.log(`📄 PPT not found in memory: ${pptId}`);
|
|
return null;
|
|
}
|
|
|
|
|
|
const slides = [];
|
|
const slidesCount = metadata.storage?.slidesCount || 0;
|
|
|
|
for (let i = 0; i < slidesCount; i++) {
|
|
const slideKey = `users/${userId}/${pptId}/slide_${String(i).padStart(3, '0')}`;
|
|
const slide = this.memoryStorage.get(slideKey);
|
|
if (slide) {
|
|
slides.push(slide);
|
|
}
|
|
}
|
|
|
|
const pptData = {
|
|
...metadata,
|
|
slides: slides,
|
|
storage: {
|
|
...metadata.storage,
|
|
loadedAt: new Date().toISOString()
|
|
}
|
|
};
|
|
|
|
console.log(`📖 Read PPT from memory: ${pptId} (${slides.length} slides)`);
|
|
return { content: pptData };
|
|
}
|
|
|
|
|
|
async savePPTToMemory(userId, pptId, pptData) {
|
|
|
|
const metadata = {
|
|
id: pptData.id || pptId,
|
|
title: pptData.title || '未命名演示文稿',
|
|
theme: pptData.theme,
|
|
viewportSize: pptData.viewportSize || 1000,
|
|
viewportRatio: pptData.viewportRatio || 0.5625,
|
|
createdAt: pptData.createdAt || new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
storage: {
|
|
type: 'folder',
|
|
version: '2.0',
|
|
slidesCount: pptData.slides?.length || 0
|
|
}
|
|
};
|
|
|
|
const metaKey = `users/${userId}/${pptId}/meta`;
|
|
this.memoryStorage.set(metaKey, metadata);
|
|
|
|
|
|
const slides = pptData.slides || [];
|
|
let totalSize = 0;
|
|
|
|
for (let i = 0; i < slides.length; i++) {
|
|
const slide = slides[i];
|
|
const compressedSlide = await this.compressSlide(slide);
|
|
const slideKey = `users/${userId}/${pptId}/slide_${String(i).padStart(3, '0')}`;
|
|
this.memoryStorage.set(slideKey, compressedSlide);
|
|
|
|
totalSize += Buffer.byteLength(JSON.stringify(compressedSlide), 'utf8');
|
|
}
|
|
|
|
console.log(`💾 Saved PPT to memory: ${pptId} (${slides.length} slides, ${(totalSize / 1024).toFixed(2)} KB)`);
|
|
|
|
return {
|
|
success: true,
|
|
storage: 'folder',
|
|
slidesCount: slides.length,
|
|
size: totalSize
|
|
};
|
|
}
|
|
|
|
|
|
async deletePPTFromMemory(userId, pptId) {
|
|
let deleted = 0;
|
|
const prefix = `users/${userId}/${pptId}/`;
|
|
|
|
|
|
for (const key of this.memoryStorage.keys()) {
|
|
if (key.startsWith(prefix)) {
|
|
this.memoryStorage.delete(key);
|
|
deleted++;
|
|
}
|
|
}
|
|
|
|
console.log(`📝 Memory storage: Deleted ${deleted} files for PPT ${pptId}`);
|
|
return deleted > 0;
|
|
}
|
|
|
|
|
|
async getUserPPTListFromMemory(userId) {
|
|
const results = [];
|
|
const userPrefix = `users/${userId}/`;
|
|
const pptIds = new Set();
|
|
|
|
|
|
for (const key of this.memoryStorage.keys()) {
|
|
if (key.startsWith(userPrefix) && key.includes('/meta')) {
|
|
const pptId = key.replace(userPrefix, '').replace('/meta', '');
|
|
pptIds.add(pptId);
|
|
}
|
|
}
|
|
|
|
|
|
for (const pptId of pptIds) {
|
|
const metaKey = `${userPrefix}${pptId}/meta`;
|
|
const metadata = this.memoryStorage.get(metaKey);
|
|
|
|
if (metadata) {
|
|
results.push({
|
|
name: pptId,
|
|
title: metadata.title || '未命名演示文稿',
|
|
updatedAt: metadata.updatedAt || new Date().toISOString(),
|
|
slidesCount: metadata.storage?.slidesCount || 0,
|
|
isChunked: false,
|
|
storageType: 'folder',
|
|
size: JSON.stringify(metadata).length,
|
|
repoIndex: 0
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(`📋 Found ${results.length} PPTs in memory for user ${userId}`);
|
|
return results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
}
|
|
|
|
|
|
async getUserPPTList(userId) {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
return await this.getUserPPTListFromMemory(userId);
|
|
}
|
|
|
|
const pptList = [];
|
|
|
|
|
|
for (let repoIndex = 0; repoIndex < this.repositories.length; repoIndex++) {
|
|
try {
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
const userDirPath = `users/${userId}`;
|
|
|
|
const response = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Get user directory for ${userId} in repo ${repoIndex}`);
|
|
|
|
|
|
const pptFolders = response.data.filter(item =>
|
|
item.type === 'dir'
|
|
);
|
|
|
|
for (const folder of pptFolders) {
|
|
try {
|
|
const pptId = folder.name;
|
|
|
|
|
|
const metaResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}/${pptId}/meta.json`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 15000
|
|
}
|
|
);
|
|
}, `Get metadata for PPT ${pptId}`);
|
|
|
|
const metaContent = Buffer.from(metaResponse.data.content, 'base64').toString('utf8');
|
|
const metadata = JSON.parse(metaContent);
|
|
|
|
pptList.push({
|
|
name: pptId,
|
|
title: metadata.title || '未命名演示文稿',
|
|
updatedAt: metadata.updatedAt || new Date().toISOString(),
|
|
slidesCount: metadata.storage?.slidesCount || 0,
|
|
isChunked: false,
|
|
storageType: 'folder',
|
|
size: metaResponse.data.size || 0,
|
|
repoIndex: repoIndex
|
|
});
|
|
} catch (error) {
|
|
console.warn(`跳过无效PPT文件夹 ${folder.name}:`, error.message);
|
|
}
|
|
}
|
|
|
|
|
|
const jsonFiles = response.data.filter(file =>
|
|
file.type === 'file' &&
|
|
file.name.endsWith('.json') &&
|
|
!file.name.includes('_chunk_')
|
|
);
|
|
|
|
for (const file of jsonFiles) {
|
|
try {
|
|
const pptId = file.name.replace('.json', '');
|
|
|
|
|
|
if (pptList.some(p => p.name === pptId)) {
|
|
continue;
|
|
}
|
|
|
|
const fileResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${userDirPath}/${file.name}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 15000
|
|
}
|
|
);
|
|
}, `Get legacy PPT file ${file.name}`);
|
|
|
|
const content = Buffer.from(fileResponse.data.content, 'base64').toString('utf8');
|
|
const pptData = JSON.parse(content);
|
|
|
|
pptList.push({
|
|
name: pptId,
|
|
title: pptData.title || '未命名演示文稿',
|
|
updatedAt: pptData.updatedAt || new Date().toISOString(),
|
|
slidesCount: pptData.isChunked ? pptData.totalSlides : (pptData.slides?.length || 0),
|
|
isChunked: pptData.isChunked || false,
|
|
storageType: 'legacy',
|
|
size: file.size,
|
|
repoIndex: repoIndex
|
|
});
|
|
} catch (error) {
|
|
console.warn(`跳过无效文件 ${file.name}:`, error.message);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`仓库 ${repoIndex} 中没有找到用户目录或访问失败:`, error.message);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
return pptList.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
}
|
|
|
|
|
|
async deletePPT(userId, pptId, repoIndex = 0) {
|
|
await this.ensureInitialized();
|
|
|
|
if (this.useMemoryStorage) {
|
|
return await this.deletePPTFromMemory(userId, pptId);
|
|
}
|
|
|
|
const repoUrl = this.repositories[repoIndex];
|
|
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
|
const pptFolderPath = `users/${userId}/${pptId}`;
|
|
|
|
try {
|
|
console.log(`🗑️ Deleting PPT folder: ${pptFolderPath}`);
|
|
|
|
|
|
const folderResponse = await this.makeGitHubRequest(async () => {
|
|
return await axios.get(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${pptFolderPath}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Get PPT folder contents for deletion: ${pptId}`);
|
|
|
|
|
|
for (const file of folderResponse.data) {
|
|
try {
|
|
await this.makeGitHubRequest(async () => {
|
|
return await axios.delete(
|
|
`${this.apiUrl}/repos/${owner}/${repo}/contents/${file.path}`,
|
|
{
|
|
data: {
|
|
message: `Delete PPT file: ${file.name}`,
|
|
sha: file.sha
|
|
},
|
|
headers: {
|
|
'Authorization': `token ${this.token}`,
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
}, `Delete file ${file.name}`);
|
|
|
|
console.log(`✅ Deleted file: ${file.name}`);
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, this.apiRateLimit.minDelay));
|
|
} catch (error) {
|
|
console.warn(`⚠️ Failed to delete file ${file.name}:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log(`✅ PPT folder deleted successfully: ${pptId}`);
|
|
return true;
|
|
|
|
} catch (error) {
|
|
if (error.response?.status === 404) {
|
|
console.log(`📄 PPT folder not found, trying legacy format: ${pptId}`);
|
|
return await this.deleteFile(userId, `${pptId}.json`, repoIndex);
|
|
}
|
|
|
|
console.error(`❌ Delete PPT failed for ${pptId}:`, error);
|
|
throw new Error(`Failed to delete PPT: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new GitHubService(); |