Files
upstream-rocketchat/apps/meteor/server/ufs/ufs-server.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

296 lines
7.5 KiB
TypeScript

import fs from 'node:fs';
import stream from 'node:stream';
import { pipeline } from 'node:stream/promises';
import URL from 'node:url';
import zlib from 'node:zlib';
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import mkdirp from 'mkdirp';
import { UploadFS } from './ufs';
Meteor.startup(() => {
const path = UploadFS.config.tmpDir;
const mode = UploadFS.config.tmpDirPermissions;
fs.stat(path, (err) => {
if (err) {
// Create the temp directory
mkdirp(path, { mode })
.then(() => {
console.log(`ufs: temp directory created at "${path}"`);
})
.catch((err) => {
console.error(`ufs: cannot create temp directory at "${path}" (${err.message})`);
});
} else {
// Set directory permissions
fs.chmod(path, mode, (err) => {
err && console.error(`ufs: cannot set temp directory permissions ${mode} (${err.message})`);
});
}
});
});
// Listen HTTP requests to serve files
WebApp.connectHandlers.use(async (req, res, next) => {
// Quick check to see if request should be caught
if (!req.url?.includes(`/${UploadFS.config.storesPath}/`)) {
next();
return;
}
// Remove store path
const parsedUrl = URL.parse(req.url, true);
const path = parsedUrl.pathname?.substr(UploadFS.config.storesPath.length + 1);
if (!path) {
next();
return;
}
const allowCORS = () => {
// res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Methods', 'POST');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
};
if (req.method === 'OPTIONS') {
const regExp = new RegExp('^/([^/?]+)/([^/?]+)$');
const match = regExp.exec(path);
// Request is not valid
if (match === null) {
res.writeHead(400);
res.end();
return;
}
// Get store
const store = UploadFS.getStore(match[1]);
if (!store) {
res.writeHead(404);
res.end();
return;
}
// If a store is found, go ahead and allow the origin
allowCORS();
next();
} else if (req.method === 'POST') {
res.writeHead(404);
res.end();
} else if (req.method === 'GET') {
// Get store, file Id and file name
const regExp = new RegExp('^/([^/?]+)/([^/?]+)(?:/([^/?]+))?$');
const match = regExp.exec(path);
// Avoid 504 Gateway timeout error
// if file is not handled by UploadFS.
if (match === null) {
next();
return;
}
// Get store
const storeName = match[1];
const store = UploadFS.getStore(storeName);
if (!store) {
res.writeHead(404);
res.end();
return;
}
if (store.onRead !== null && store.onRead !== undefined && typeof store.onRead !== 'function') {
console.error(`ufs: Store.onRead is not a function in store "${storeName}"`);
res.writeHead(500);
res.end();
return;
}
// Remove file extension from file Id
const index = match[2].indexOf('.');
const fileId = index !== -1 ? match[2].substr(0, index) : match[2];
// Get file from database
const file = await store.getCollection().findOne({ _id: fileId });
if (!file) {
res.writeHead(404);
res.end();
return;
}
try {
// Check if the file can be accessed
if ((await store.onRead.call(store, fileId, file, req, res)) !== false) {
const options: {
start?: number;
end?: number;
} = {};
let status = 200;
// Prepare response headers
const headers: Record<string, any> = {
'Content-Type': file.type,
'Content-Length': file.size,
};
// Add ETag header
if (typeof file.etag === 'string') {
headers.ETag = file.etag;
}
// Add Last-Modified header
if (file.modifiedAt instanceof Date) {
headers['Last-Modified'] = file.modifiedAt.toUTCString();
} else if (file.uploadedAt instanceof Date) {
headers['Last-Modified'] = file.uploadedAt.toUTCString();
}
// Parse request headers
if (typeof req.headers === 'object') {
// Compare ETag
if (req.headers['if-none-match']) {
if (file.etag === req.headers['if-none-match']) {
res.writeHead(304); // Not Modified
res.end();
return;
}
}
// Compare file modification date
if (req.headers['if-modified-since']) {
const modifiedSince = new Date(req.headers['if-modified-since']);
if (
(file.modifiedAt instanceof Date && file.modifiedAt > modifiedSince) ||
(file.uploadedAt instanceof Date && file.uploadedAt > modifiedSince)
) {
res.writeHead(304); // Not Modified
res.end();
return;
}
}
// Support range request
if (typeof req.headers.range === 'string') {
const { range } = req.headers;
// Range is not valid
if (!range) {
res.writeHead(416);
res.end();
return;
}
const total = file.size || 0;
const unit = range.substr(0, range.indexOf('='));
if (unit !== 'bytes') {
res.writeHead(416);
res.end();
return;
}
const ranges = range
.substr(unit.length)
.replace(/[^0-9\-,]/, '')
.split(',');
if (ranges.length > 1) {
// todo: support multipart ranges: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
} else {
const r = ranges[0].split('-');
const start = parseInt(r[0], 10);
const end = r[1] ? parseInt(r[1], 10) : total - 1;
// Range is not valid
if (start < 0 || end >= total || start > end) {
res.writeHead(416);
res.end();
return;
}
// Update headers
headers['Content-Range'] = `bytes ${start}-${end}/${total}`;
headers['Content-Length'] = end - start + 1;
options.start = start;
options.end = end;
}
status = 206; // partial content
}
} else {
headers['Accept-Ranges'] = 'bytes';
}
// Open the file stream
const rs = await store.getReadStream(fileId, file, options);
const ws = new stream.PassThrough();
rs.on('error', (err) => {
store.onReadError.call(store, err, fileId, file);
res.end();
});
ws.on('error', (err) => {
store.onReadError.call(store, err, fileId, file);
res.end();
});
ws.on('close', () => {
// Close output stream at the end
ws.emit('end');
});
// Transform stream
store.transformRead(rs, ws, fileId, file, req, headers);
// Parse request headers
if (typeof req.headers === 'object') {
// Compress data using if needed (ignore audio/video as they are already compressed)
if (typeof req.headers['accept-encoding'] === 'string' && (!file.type || !/^(audio|video)/.test(file.type))) {
const accept = req.headers['accept-encoding'];
// Compress with gzip
if (accept.match(/\bgzip\b/)) {
headers['Content-Encoding'] = 'gzip';
delete headers['Content-Length'];
res.writeHead(status, headers);
await pipeline(ws, zlib.createGzip(), res);
return;
}
// Compress with deflate
if (accept.match(/\bdeflate\b/)) {
headers['Content-Encoding'] = 'deflate';
delete headers['Content-Length'];
res.writeHead(status, headers);
await pipeline(ws, zlib.createDeflate(), res);
return;
}
}
}
// Send raw data
if (!headers['Content-Encoding']) {
res.writeHead(status, headers);
await pipeline(ws, res);
}
} else {
res.end();
}
} catch (err) {
console.error(`ufs: ${err instanceof Error ? err.message : String(err)}`);
if (!res.headersSent) {
res.writeHead(500);
}
if (!res.writableEnded) {
res.end();
}
}
} else {
next();
}
});