Files
Socialflow/server/services/scheduler.ts
T
MichaelandClaude Sonnet 4.6 7733acbc22 feat: Replace external storage with local file storage + auto cleanup
Media is now stored directly on the server disk (no external service).
Files are served via Express static middleware at /uploads/*.

Cleanup rules:
- Videos deleted immediately after successful publish to Facebook/Instagram
- Videos older than 7 days purged daily by cron job
- Images older than 30 days purged daily by cron job

Changes:
- server/services/minio.ts: replaced S3 client with local fs read/write
- server/index.ts: added static serving for /uploads/media, /logos, /stories
- server/services/media-purge.ts: new rules (images 30d, videos 7d)
- server/services/scheduler.ts: delete local videos after successful publish
- docker-compose.yml: add media_uploads, logos_uploads, stories_uploads volumes
- settings UI: removed credentials card, shows local storage info
- package.json: removed googleapis (unneeded)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 07:35:27 +01:00

145 lines
5.1 KiB
TypeScript

import cron, { ScheduledTask } from "node-cron";
import { storage } from "../storage";
import { facebookService } from "./facebook";
import { minioService as storageService } from "./minio";
export class SchedulerService {
private task: ScheduledTask | null = null;
start() {
// Run every minute to check for pending posts
this.task = cron.schedule("* * * * *", async () => {
await this.processPendingPosts();
});
console.log("Scheduler service started");
}
stop() {
if (this.task) {
this.task.stop();
console.log("Scheduler service stopped");
}
}
private async processPendingPosts() {
try {
const pendingPosts = await storage.getPendingScheduledPosts();
for (const scheduledPost of pendingPosts) {
try {
// Reject legacy "both" posts - they should have been split at creation
// New posts are automatically split in /api/posts
if (scheduledPost.postType === 'both') {
const errorMsg = 'Legacy postType "both" is no longer supported. Deleting this scheduled entry. Please recreate this post - it will automatically be split into separate feed and story posts.';
console.error(`Rejecting and deleting legacy "both" post ${scheduledPost.id}: ${errorMsg}`);
// Delete the legacy post entirely (don't mark as published - that's misleading)
await storage.deleteScheduledPost(scheduledPost.id);
// Update parent post status to draft so user can reschedule
await storage.updatePost(scheduledPost.postId, {
status: "draft",
});
continue;
}
await this.publishPost(scheduledPost);
} catch (error) {
console.error(`Error publishing post ${scheduledPost.id}:`, error);
await storage.updateScheduledPost(scheduledPost.id, {
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
} catch (error) {
console.error("Error processing pending posts:", error);
}
}
private async publishPost(scheduledPost: any) {
const post = await storage.getPost(scheduledPost.postId);
const page = await storage.getSocialPage(scheduledPost.pageId);
if (!post || !page) {
throw new Error("Post or page not found");
}
console.log(`Publishing post ${post.id} to ${page.platform} page ${page.pageName}`);
// Get all media for the post (for multi-photo carousels)
const postMediaList = await storage.getPostMedia(post.id);
const mediaList: any[] = [];
for (const pm of postMediaList) {
const media = await storage.getMediaById(pm.mediaId);
if (media) {
mediaList.push(media);
}
}
// Validate that story posts have media
// Note: "both" should never reach here since they're split at creation
if (scheduledPost.postType === 'story' && mediaList.length === 0) {
const errorMsg = 'Les stories nécessitent un média. Ce post ne peut pas être publié.';
console.error(`Deleting invalid story post ${scheduledPost.id} without media: ${errorMsg}`);
// Delete the invalid scheduled post (don't mark as published - that's misleading)
await storage.deleteScheduledPost(scheduledPost.id);
// Set post to draft so user can fix and reschedule
await storage.updatePost(post.id, {
status: "draft",
});
return;
}
// Publish to Facebook/Instagram
let externalPostId: string;
if (page.platform === 'facebook') {
externalPostId = await facebookService.publishPost(post, page, scheduledPost.postType, mediaList);
} else if (page.platform === 'instagram') {
// Instagram publishing not yet implemented
console.warn(`Instagram publishing not yet implemented for post ${post.id}`);
await storage.updateScheduledPost(scheduledPost.id, {
error: 'Instagram publishing not yet implemented',
});
await storage.updatePost(post.id, {
status: "failed",
});
return;
} else {
throw new Error(`Unsupported platform: ${page.platform}`);
}
// Update the scheduled post as published (clear any previous error)
await storage.updateScheduledPost(scheduledPost.id, {
publishedAt: new Date(),
externalPostId,
error: null,
});
// Update post status
await storage.updatePost(post.id, {
status: "published",
});
// Delete local video files immediately after successful publishing
for (const mediaItem of mediaList) {
if (mediaItem.type === 'video' && mediaItem.cloudinaryPublicId) {
try {
await storageService.deleteMedia(mediaItem.cloudinaryPublicId, mediaItem.userId, 'video');
console.log(`[Scheduler] Deleted local video file for media ${mediaItem.id}`);
} catch (err) {
console.warn(`[Scheduler] Failed to delete local video file for media ${mediaItem.id}:`, err);
}
}
}
console.log(`Successfully published post ${post.id} with external ID: ${externalPostId}`);
}
}
export const schedulerService = new SchedulerService();