11const express = require ( 'express' ) ;
22const multer = require ( 'multer' ) ;
33const crypto = require ( 'crypto' ) ;
4+ const zlib = require ( 'zlib' ) ;
45const fs = require ( 'fs' ) ;
56const path = require ( 'path' ) ;
67
7- const app = express ( ) ;
8- app . use ( express . json ( { limit : '25mb' } ) ) ;
9-
108const PORT = process . env . PORT || 3200 ;
119const FILES_DIR = process . env . FILES_DIR || '/data/files' ;
1210const META_DIR = process . env . META_DIR || '/data/meta' ;
1311const 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 ────────────
8185app . 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
314339app . 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