为什么 Node 会屏蔽流式文件?

Why does Node block the streamed file?

我正在编写一个通过流向客户端发送视频文件的函数。它工作正常,除了在每个请求中它打开一个文件描述符但从不关闭它。这意味着这些文件以后无法删除,因为它们被节点进程阻止了。

这是导致这种情况的函数:

exports.stream = (req, res, next) => {
    const range = req.headers.range;
    const volumeName = req.query.volume;
    const folderPath = req.query.folder;
    const fileName = req.query.file;
    if (!range) return res.sendStatus(416);
    try {
        let volume = Services.volumes.get.byName(volumeName);
        let path = volume.location + folderPath + fileName;
        let pathExists = Services.files.get.exists(path);
        if (!pathExists) return res.status(404).send('unable to stream file, path not exists');
        
        let stats = fs.statSync(path);
        if (!stats.isFile()) {
            return res.status(400).send('requested path is not a file: ' + path);
        }
        const fileSize = stats.size;

        const parts = range.replace(/bytes=/, "").split("-");
        const start = parseInt(parts[0], 10);
        let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
        let chunksize = (end - start) + 1;
        const maxChunk = 1024 * 1024;
        if (chunksize > maxChunk) {
            end = start + maxChunk - 1;
            chunksize = (end - start) + 1;
        }

        res.writeHead(206, {
            'Content-Range': `bytes ${start}-${end}/${fileSize}`,
            'Accept-Ranges': 'bytes',
            'Content-Length': chunksize,
            'Content-Type': 'video/mp4',
        });

        let stream = fs.createReadStream(path, {start: start, end: end, autoClose: true});
        stream.on('open', function () {
            stream.pipe(res);
        });
        stream.on('error', function (err) {
            res.end(err);
        });
        stream.on('close', function () {
            stream.destroy();
        });
    } catch (e) {
        next(new Error('unable to stream path: ' + e));
    }
};

我找到了防止流打开的解决方案。

  • 一方面,我不得不限制每个请求中发送的信息量(又名chunksize)。
  • 另一方面,我使用 pipeline instead of pipe. In theory both things are the same but in practice they work differently. This link 作为起点。

代码如下(还是要写的好看):

exports.stream = ({config}) => (req, res, next) => {
    let volumeName = req.query.volume;
    let folderPath = req.query.folder;
    let fileName = req.query.file;
    try {
        let volume = Services.volumes.get.byName(volumeName);
        let path = volume.location + folderPath + fileName;
        let exists = Services.files.get.exists(path);
        if (!exists) {
            return res.status(404).send('unable to stream file, path not exists');
        }

        let stats = fs.statSync(path);
        if (!stats.isFile()) {
            return res.status(400).send('requested path is not a file: ' + path);
        }

        const fileSize = stats.size;
        let maxchunksize = 1024 * 1024;
        let headers = {};
        let readable;

        if (req.headers['range']) {
            const range = req.headers.range;
            const parts = range.replace(/bytes=/, "").split("-");
            let start = parseInt(parts[0], 10);
            let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
            let chunksize = (end - start) + 1;
            if (start > end || start < 0 || end > fileSize - 1) {
                headers['Content-Range'] = '*/' + fileSize;
                res.writeHead(416, headers);
                return res.end();
            }
            if (chunksize > maxchunksize) {
                end = start + maxchunksize - 1;
                chunksize = (end - start) + 1;
            }
            headers['Accept-Ranges'] = 'bytes';
            headers['Content-Type'] = 'video/mp4';
            headers['Content-Range'] = 'bytes ' + start + '-' + end + '/' + fileSize;
            headers['Content-Length'] = chunksize;
            res.writeHead(206, headers);
            readable = fs.createReadStream(path, {start: start, end: end, autoClose: true});

        } else {
            headers['Content-Length'] = fileSize;
            headers['Content-Type'] = 'video/mp4';
            res.writeHead(200, headers);
            readable = fs.createReadStream(path);
        }

        stream.pipeline(readable, res, err => {
            if (err) console.log(err);
        });
    } catch (e) {
        next(new Error('unable to stream path: ' + e));
    }
};