Upload files với nodejs restify (phần 5)

0
53
views
upload-files-with-restify

Tại sao phải upload files?

Có thể nói 90% các ứng dụng web hiện nay đều cần đến upload files lên server, có thể là upload ảnh, video và các file văn bản khác. Nếu như bài trước chúng ta đã nghiên cứu cách sử dụng event thì hôm nay mình sẽ hướng dẫn các bạn xây dựng api để upload file một cách đơn giản nhất. Ngoài ra ta có thể crop ảnh thumbnail với file dạng ảnh, hay lấy ảnh thumbnail và đọc độ dài từ file video …

Quản lý thư mục upload

Chúng ta sẽ có một thư mục chung uploads nhằm phân biệt với các thư mục mã nguồn khác. Các đơn giản nhất là tất cả các file sau khi upload lên đều được lưu chung vào thư mục upload vừa nói ở trên. Tuy nhiên, cách này rất khó quản lý vì tất cả các file đề nằm chung thư mục, khi số lượng file càng ngày càng lớn thì càng khó quản lý và tốc độ truy xuất càng giảm. Ta sẽ lưu các file upload theo username của người dùng, trong các thư mục này file lại được phân chia theo ngày tháng năm nó được upload. Việc chia nhỏ này giúp quản lý các file dễ dàng hơn cả trong việc khôi phục hay xóa hàng loạt mà không ảnh hưởng đến người dùng khác.

Restify sử dụng formidable để parse các request kiểu multipart/form-data – là content type phổ biến khi upload file – do đó bạn hoàn toàn có thể sử dụng các options của formidable trong quá trình upload, hoặc để giá trị mặc định nếu không chắc chắn. Tạo route /api/v1/uploadFiles trong file utilRouter.js như sau :

const mkdirp = require('mkdirp');
const fs = require('fs');
const path = require('path');
const uuid = require('uuid');

server.post('/api/v1/uploadFiles', (req, res, next) => {
    let user = AuthorizationService.getUser(req);
    let username, newFilePath, today = new Date(), files = [];

    if (user) {
        username = user.username;
    } else username = 'anonymous';

    for (let item in req.files) {
        let file = req.files[item];
        newFilePath = path.join(__basedir, '/upload/', username, '/', today.getFullYear().toString(), '/', (today.getMonth() + 1).toString() , '/', 
                      today.getDate().toString());

        mkdirp(newFilePath, function (err) {
            if (err) {
                log.error(err);
                return next(err);
            } else {
                let ext = path.extname(file.name);
                newFilePath = path.join(newFilePath, '/', uuid.v4() +  ext);
                fs.renameSync(file.path, newFilePath);

                files.push({ link: newFilePath, name: file.name, size: file.size });
            }
        });
    }

    res.send(201, files);
    next();
}

mkdirp là thư viện dùng để tạo thư mục nhiều level cùng một lúc. Khi request dạng multipart/form-data được parse thành công, file upload sẽ được lưu trong thư mục tạm (/tmp) tùy thuộc vào từng hệ điều hành. Việc của chúng ta là copy file đó vào cấu trúc thư mục đã xác định trước. Cấu trúc thư mục hiện tại sẽ có dạng : /uploads/<username>/<năm>/<tháng>/<ngày>/<filename>. Filename sẽ được tạo ngẫu nhiên theo chuẩn UUID – là gần như duy nhất.

Mặc định thì file lớn nhất mà các bạn có thể upload đó là 20MB, nếu upload file lớn hơn bạn sẽ gặp lỗi

BadRequestError: maxFileSize exceeded, received 2098372 bytes of file data

Để fix lỗi này ta sửa file index.js như sau :

const restify = require('restify');
const MAX_UPLOAD_FILE_SIZE = 4 * 1024 * 1024 * 1024;

server.use(restify.plugins.bodyParser({ maxBodySize: MAX_UPLOAD_FILE_SIZE, maxFileSize: MAX_UPLOAD_FILE_SIZE }));

bodyParser là thư viện có sắn của restify có nhiệm vụ parse các body có dạng multipart/form-data, với cấu hình như trên ta có thể upload một file tối đa là 4GB. Tùy vào nhu cầu bạn có thể thay đổi thông số này.

Để crop một ảnh làm thumbnail ta dùng lib imagemagick, ta sẽ thêm một bước crop trước khi send request về client.

const im = require('imagemagick');

fs.renameSync(file.path, newFilePath);
im.crop({
    srcPath: newFilePath,
    dstPath: newFilePath,
    width: 276,
    height: 150,
    quality: 1,
    gravity: 'Center'
}, function(err, stdout, stderr){
    if (err) {
        log.error(err);
    }
});

ở đây ta đã để file crop đè lên file gốc, nếu muốn file crop không đè lên file gốc, ta phải truyền một path khác vào cho tham số dspPath. Với các file video ta có thể đọc được độ dài của video, chọn môt thời điểm trong video và chụp lại màn hình để làm ảnh thumbnail. Để đọc thông tin video ta dùng thư viện ffprobe kết hợp với ffprobe-static. Sửa lại từ đoạn renameSync như sau :

const ffprobe = require('ffprobe');
const ffprobeStatic = require('ffprobe-static');

fs.renameSync(file.path, newFilePath);
let totalTime = 0, basename = uuid.v4();
let thumbnail = path.join(newFilePath, '/', basename + '.jpg');

ffprobe(newFilePath, { path: ffprobeStatic.path }, function (err, info) {
    if (!err) {
        if (info.streams) {
            info.streams.map(stream => {
                totalTime = parseInt(stream.duration.toString());
            });

            files.push({
                url: url,
                duration: totalTime,
                name: file.name,
                status: 200
            });

            res.send(201, files);
            next();
        }
     } else {
        log.error(err);
     }
 });

Để trích xuất thumbnail từ video ra dùng thư viện video-thumb, đầu vào là một thời điểm trong video có dạng hh:mm:ss, nó phải lớn hơn 0 và nhỏ hơn độ dài video. Để xác định thời điểm này này dùng phương pháp tương đối, ví dụ thời điểm chạy được 20%. Đầu tiên ta xây dựng một hàm nhận vào số giây và trả về text có dạng hh:mm:ss

function elapsed(seconds) {
    let result = '';

    let hour = Math.floor(seconds / 3600);
    if (hour > 0 && hour < 10) {
        result = result.concat('0', hour.toString(), ':');
    } else if (hour > 10) {
        result = result.concat(hour.toString(), ':');
    } else {
        result = result.concat('00', ':');
    }

    let minute = Math.floor((seconds - hour * 3600) / 60);

    if (minute > 10) {
        result = result.concat(minute.toString(), ':');
    } else if (minute > 0 && minute < 10) {
        result = result.concat('0', minute.toString(), ':');
    } else {
        result = result.concat('00', ':');
    }

    let second = seconds - 3600 * hour - 60 * minute;
    if (second >= 10) {
        result = result.concat(second.toString());
    } else {
        result = result.concat('0', second.toString());
    }

    return result;
}

Sửa lại hàm từ đoạn get info.streams

const thumbler = require('video-thumb');


let totalTime = 0, basename = uuid.v4();
let thumbnail = path.join(newFilePath, '/', basename + '.jpg');

ffprobe(newFilePath, { path: ffprobeStatic.path }, function (err, info) {
    if (!err) {
        if (info.streams) {
            info.streams.map(stream => {
                totalTime = parseInt(stream.duration.toString());
            });

            let time = elapsed(Math.round(totalTime * 0.2));
            thumbler.extract(newFilePath, thumbnail, time, '240x135', function(error, image) {
                if (error) {
                    log.error(error);
                }
            });

            files.push({
                url: url,
                duration: totalTime,
                thumbnail: thumbnail,
                name: file.name,
                status: 200
            });

            res.send(201, files);
            next();
        }
     } else {
        log.error(err);
     }
 });

Trong các thông tin lấy được từ video còn có video resolution, tức là thông số 720p hay 1080p mà ta thường hay nghe. Tuy nhiên, không phải video loại nào cũng có thể lấy được thông tin này. Theo mình nhớ thì file flv không có thông tin này. Sửa hàm trên như sau :

let codedHeight = 0;

ffprobe(newFilePath, { path: ffprobeStatic.path }, function (err, info) {
    if (!err) {
        if (info.streams) {
            info.streams.map(stream => {
                totalTime = parseInt(stream.duration.toString());
                if (stream.codec_type === 'video' && stream.coded_height > codedHeight) {
                    codedHeight = stream.coded_height;
                }
            });

            let time = elapsed(Math.round(totalTime * 0.2));
            thumbler.extract(newFilePath, thumbnail, time, '240x135', function(error, image) {
                if (error) {
                    log.error(error);
                }
            });

            files.push({
                url: url,
                duration: totalTime,
                thumbnail: thumbnail,
                name: file.name,
                status: 200, 
                resolution: codedHeight
            });

            res.send(201, files);
            next();
        }
     } else {
        log.error(err);
     }
 });

BÌNH LUẬN

Please enter your comment!
Please enter your name here