Skip to content

Commit 6317ea3

Browse files
fix(file-bridge): transparent gzip-at-rest + raise limits (config-backup 413)
The config-backup full backup outgrew the file-bridge upload caps and failed with 413, so automatic backups had been failing since ~2026-06-01. - /upload/base64 accepts compress:true -> gzip on disk; GET /files/:id and /forward decompress on the fly, so callers always receive the original bytes (transparent, no breaking change; old/other uploads unaffected) - express body limit derived from MAX_FILE_SIZE_MB (was hardcoded 25mb, below the file cap); MAX_FILE_SIZE_MB 20 -> 200 in docker-compose - bump config-backup CDN pin to @6089521 (gzip + agents->claw_agents fix) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 94237f8 commit 6317ea3

3 files changed

Lines changed: 39 additions & 14 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ services:
157157
- file_bridge_data:/data
158158
environment:
159159
- UPLOAD_TTL_HOURS=24
160-
- MAX_FILE_SIZE_MB=20
160+
- MAX_FILE_SIZE_MB=200
161161
networks:
162162
- n8n-claw-net
163163

file-bridge/server.js

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
const express = require('express');
22
const multer = require('multer');
33
const crypto = require('crypto');
4+
const zlib = require('zlib');
45
const fs = require('fs');
56
const path = require('path');
67

7-
const app = express();
8-
app.use(express.json({ limit: '25mb' }));
9-
108
const PORT = process.env.PORT || 3200;
119
const FILES_DIR = process.env.FILES_DIR || '/data/files';
1210
const META_DIR = process.env.META_DIR || '/data/meta';
1311
const TTL_HOURS = parseInt(process.env.UPLOAD_TTL_HOURS || '24', 10);
14-
const MAX_SIZE = parseInt(process.env.MAX_FILE_SIZE_MB || '20', 10) * 1024 * 1024;
12+
const MAX_SIZE = parseInt(process.env.MAX_FILE_SIZE_MB || '200', 10) * 1024 * 1024;
13+
// Express body limit must hold the base64 payload (~1.34x original) plus JSON wrapper.
14+
// Derived from MAX_FILE_SIZE_MB with headroom; override with MAX_BODY_SIZE_MB if needed.
15+
const MAX_BODY_MB = parseInt(process.env.MAX_BODY_SIZE_MB || String(Math.ceil((MAX_SIZE / 1024 / 1024) * 1.4) + 16), 10);
16+
17+
const app = express();
18+
app.use(express.json({ limit: MAX_BODY_MB + 'mb' }));
1519

1620
// Ensure directories exist
1721
[FILES_DIR, META_DIR].forEach(dir => {
@@ -79,26 +83,37 @@ app.post('/upload', upload.single('file'), (req, res) => {
7983

8084
// ── POST /upload/base64 — JSON with base64 content ────────────
8185
app.post('/upload/base64', (req, res) => {
82-
const { content_base64, file_name, mime_type } = req.body;
86+
const { content_base64, file_name, mime_type, compress } = req.body;
8387
if (!content_base64) {
8488
return res.status(400).json({ error: 'content_base64 is required.' });
8589
}
8690

87-
const buffer = Buffer.from(content_base64, 'base64');
88-
if (buffer.length > MAX_SIZE) {
91+
const original = Buffer.from(content_base64, 'base64');
92+
if (original.length > MAX_SIZE) {
8993
return res.status(413).json({ error: `File too large. Max ${MAX_SIZE / 1024 / 1024} MB.` });
9094
}
9195

96+
// Optional gzip-at-rest. Transparent: downloads and forwards are decompressed on the fly,
97+
// so callers always receive the original bytes. Keeps large JSON backups small on disk.
98+
let stored = original;
99+
let encoding = null;
100+
if (compress) {
101+
stored = zlib.gzipSync(original);
102+
encoding = 'gzip';
103+
}
104+
92105
const id = generateId();
93106
const expiresAt = new Date(Date.now() + TTL_HOURS * 3600 * 1000).toISOString();
94107

95-
fs.writeFileSync(filePath(id), buffer);
108+
fs.writeFileSync(filePath(id), stored);
96109

97110
const meta = {
98111
id,
99112
file_name: file_name || 'upload',
100113
mime_type: mime_type || 'application/octet-stream',
101-
size_bytes: buffer.length,
114+
size_bytes: original.length,
115+
stored_bytes: stored.length,
116+
encoding,
102117
created_at: new Date().toISOString(),
103118
expires_at: expiresAt
104119
};
@@ -123,6 +138,14 @@ app.get('/files/:id', (req, res) => {
123138

124139
res.set('Content-Type', meta.mime_type);
125140
res.set('Content-Disposition', `attachment; filename="${meta.file_name}"`);
141+
142+
// Stored gzipped (e.g. config backups): decompress on the fly so callers get the original bytes.
143+
if (meta.encoding === 'gzip') {
144+
const buf = zlib.gunzipSync(fs.readFileSync(fp));
145+
res.set('Content-Length', buf.length);
146+
return res.end(buf);
147+
}
148+
126149
res.set('Content-Length', meta.size_bytes);
127150
fs.createReadStream(fp).pipe(res);
128151
});
@@ -189,7 +212,8 @@ app.post('/files/:id/forward', async (req, res) => {
189212
const { url, headers, form_fields, filename, file_field, method, raw_body } = req.body;
190213
if (!url) return res.status(400).json({ error: 'url is required.' });
191214

192-
const fileBuffer = fs.readFileSync(fp);
215+
const storedBuffer = fs.readFileSync(fp);
216+
const fileBuffer = meta.encoding === 'gzip' ? zlib.gunzipSync(storedBuffer) : storedBuffer;
193217
const uploadName = filename || meta.file_name;
194218
const httpMethod = (method || 'POST').toUpperCase();
195219

@@ -286,7 +310,8 @@ app.get('/health', (req, res) => {
286310
status: 'ok',
287311
files_stored: metaFiles.length,
288312
ttl_hours: TTL_HOURS,
289-
max_size_mb: MAX_SIZE / 1024 / 1024
313+
max_size_mb: MAX_SIZE / 1024 / 1024,
314+
max_body_mb: MAX_BODY_MB
290315
});
291316
});
292317

@@ -312,5 +337,5 @@ setInterval(() => {
312337
}, 5 * 60 * 1000);
313338

314339
app.listen(PORT, () => {
315-
console.log(`File Bridge running on port ${PORT} (TTL: ${TTL_HOURS}h, max: ${MAX_SIZE / 1024 / 1024}MB)`);
340+
console.log(`File Bridge running on port ${PORT} (TTL: ${TTL_HOURS}h, max file: ${MAX_SIZE / 1024 / 1024}MB, max body: ${MAX_BODY_MB}MB)`);
316341
});

0 commit comments

Comments
 (0)