feat: daily timeline advanced UI and calendar integration
This commit is contained in:
@@ -54,6 +54,59 @@ export async function createExpressApp() {
|
||||
// Add JSON parser with generous limit for render payloads (timelineElements can be large)
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// ═══ Dynamic Workspace Serving ═══
|
||||
let activeWorkspacePath = "";
|
||||
|
||||
app.post("/api/config/workspace", express.json(), (req, res) => {
|
||||
activeWorkspacePath = req.body.path;
|
||||
res.json({ success: true, path: activeWorkspacePath });
|
||||
});
|
||||
|
||||
app.use("/workspace", (req, res, next) => {
|
||||
if (activeWorkspacePath) {
|
||||
const reqPath = decodeURIComponent(req.path);
|
||||
const fullPath = path.join(activeWorkspacePath, reqPath);
|
||||
// Validate path traversal
|
||||
if (!fullPath.startsWith(activeWorkspacePath)) {
|
||||
return res.status(403).send("Forbidden");
|
||||
}
|
||||
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
||||
return res.sendFile(fullPath);
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Upload an asset directly to a brand's workspace folder
|
||||
app.post("/api/upload/brand", mediaUpload.single("file"), (req, res) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
|
||||
const { brandId, workspacePath } = req.body;
|
||||
if (!brandId || !workspacePath) {
|
||||
return res.status(400).json({ error: "brandId and workspacePath required" });
|
||||
}
|
||||
|
||||
const brandDir = path.join(workspacePath, brandId, "brand");
|
||||
if (!fs.existsSync(brandDir)) {
|
||||
fs.mkdirSync(brandDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Move file from UPLOADS_DIR to brandDir
|
||||
const ext = path.extname(req.file.originalname) || '';
|
||||
const finalFilename = `${crypto.randomUUID()}${ext}`;
|
||||
const finalPath = path.join(brandDir, finalFilename);
|
||||
|
||||
fs.renameSync(req.file.path, finalPath);
|
||||
|
||||
// Return the public URL that routes through our dynamic /workspace middleware
|
||||
const publicUrl = `http://localhost:3000/workspace/${brandId}/brand/${finalFilename}`;
|
||||
res.json({ url: publicUrl, path: finalPath });
|
||||
} catch (error) {
|
||||
console.error("Brand upload error:", error);
|
||||
res.status(500).json({ error: "Upload failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// ═══ Serve uploaded media files ═══
|
||||
app.use("/api/media", express.static(UPLOADS_DIR, {
|
||||
maxAge: "1d",
|
||||
@@ -212,7 +265,7 @@ export async function createExpressApp() {
|
||||
// Start a render job
|
||||
app.post("/api/render/start", async (req, res) => {
|
||||
try {
|
||||
const { format, width, height, fps, durationInFrames, compositionId, inputProps } = req.body;
|
||||
const { format, width, height, fps, durationInFrames, compositionId, inputProps, targetPath, brandId } = req.body;
|
||||
|
||||
if (!format || !width || !height || !compositionId) {
|
||||
return res.status(400).json({ error: "Missing required fields: format, width, height, compositionId" });
|
||||
@@ -227,6 +280,8 @@ export async function createExpressApp() {
|
||||
durationInFrames: durationInFrames || 150,
|
||||
compositionId,
|
||||
inputProps: inputProps || {},
|
||||
targetPath,
|
||||
brandId
|
||||
});
|
||||
|
||||
console.log(`🎬 Job created: ${job.id} (${format} ${width}x${height})`);
|
||||
|
||||
Reference in New Issue
Block a user