Как отменить загрузку пользователя в Formidable (Node.js)?

Я работаю над этой проблемой уже два дня, и я застрял. Я использую Node.js с Express, и я пытаюсь реализовать форму загрузки. В принципе, я хочу, чтобы форма делала следующее:

  • Проверьте размер файла и отмените загрузку, если он слишком велик (когда я говорю "отменить", я имею в виду, что любые другие данные не записываются на диск и удаляют временный файл)

  • Проверьте тип файла и убедитесь, что он правильный тип (.jpg,.png и т.д.), если это не так, а затем прекратите любую дальнейшую запись на диск и удалите временный файл.

В настоящее время у меня работает загрузка, и я испускаю ошибки, когда файл слишком велик или не соответствует правильному типу, а затем я удаляю файл с fs.unlink() после того, как весь файл был записан на диск. Но я вижу потенциальную проблему с этим подходом: что, если пользователь загружает огромный файл (размер GB)? С моим подходом прямо сейчас он будет удален из моей машины, но не после траты тонны ресурсов. Поэтому в основном я ищу использовать минимальное количество ресурсов, чтобы подтвердить, что файл подходит для загрузки. Это код, который у меня есть до сих пор:

    var path = absolutePath + '/public/images/users/' + req.session.userId + '/';
    var maxSize = 3146000; // 3MB
    var form = new formidable.IncomingForm();
    form.uploadDir = path;
    form.keepExtensions = true;

    form.on('error', function(message) {
        if(message)
        {
            res.json({err: message});
        }
        else
        {
            res.json({err: 'Upload error, please try again'});
        }
    });

    form.on('fileBegin', function(name, file){
        if(form.bytesExpected > maxSize)
        {
            this.emit('error', 'Size must not be over 3MB');
        }
    });

    form.on('file', function(name, file) {
        var type = file.type;
        type = type.split('/');
        type = type[1];

        if(type != 'jpeg' && type != 'png' && type != 'gif')
        {
            this.emit('error', "JPG's, PNG's, GIF only");
            fs.unlink(file.path);
        }
        else
        {
            fs.rename(file.path, path + 'profile.' + type);
        }
    });

    form.on('progress', function(bytesReceived, bytesExpected) {
            console.log(bytesReceived); //This is just to view progress
    });

    form.parse(req);

Я тоже смущен, потому что согласно документам https://github.com/felixge/node-formidable он говорит:

Запрос, который испытывает ошибку, автоматически приостанавливается, вам придется вручную вызвать request.resume(), если вы хотите, чтобы запрос продолжал запускать "данные".

Это было бы здорово, но я не могу заставить его работать. Всякий раз, когда я испускаю "ошибку", события "данных" продолжают стрелять до завершения.

Попытки

Я попытался отменить запрос, когда произошла ошибка, но безрезультатно. req.pause() ничего не сделал для меня, req.end() и req.abort() дал мне ошибку, говоря, что это не метод, а req.connection.destroy() и req.connection.end() просто отправили цикл запросов POST.

Заключительные мысли

Итак, то, что я ищу, похоже, должно быть обычным явлением, но последние два дня я провел в Интернете тщательную реализацию, и я не могу найти ничего. Я имею в виду, что просто проверить размер и тип файла ПОСЛЕ того, что все было загружено, но кто хочет тратить все эти ресурсы? Не говоря уже о том, что могут делать злонамеренные пользователи.

Я продолжу работу, пока не получу именно то, что ищу, но я подумал, что эта проблема может быть актуальной для некоторых других пользователей, и, надеюсь, я смогу получить помощь!

Спасибо за ваше время.

Ответ 1

Прошло некоторое время, и теперь вы можете использовать multer вместо грозного или многопартийного. У Multer есть желаемая функциональность.

Ответ 2

Я попытаюсь ответить на свой вопрос...

Итак, после некоторых проб и ошибок с Formidable я просто отказался от него и переключился на Multiparty. Multiparty ACTUALLY отменяет загрузку, когда испускается ошибка, которая была огромной.

Мое решение

Таким образом, мое решение использует проверку размера и типа клиентской стороны (не показано в коде). Затем запрос отправляется на сервер. На сервере я снова проверяю, соответствуют ли размер и тип файла ПЕРЕД ПИСЬЕМ ДИСКА. Я могу сделать это, используя событие Multiparty part. Если они неверны, я просто отправлю ответ с ошибкой 413. (Спасибо josh3736, чтобы уточнить, что должно произойти в браузере.) После отправки назад 413 поведение браузера немного спорадично. Для браузера, который я тестировал, он просто показал запрос на отправку pending. Я считаю, что такое поведение связано с тем, что вся форма формы не обрабатывается, поэтому она не будет принимать никаких ответов. Это может показаться не самым элегантным способом борьбы с ним, поскольку код ошибки не отображается, но это поведение будет встречено только злонамеренными пользователями, которые обходят проверку на стороне клиента (мой сайт зависит от Javascript, поэтому все пользователи будут иметь это если они хотят использовать мой сайт). Итак, это мое решение в словах, теперь для некоторого кода...

app.post('/profile/profile_pic', urlencoded, function (req, res) {

    var path = absolutePath + '/public/images/users/' + req.session.userId + '/';
    var maxSize = 3146000; // 3MB

    var options = {uploadDir: path};
    var form = new multiparty.Form();

    form.on('error', function(message) {
        res.status = 413;
        res.send(413, 'Upload too large');
        res.end();
    });

    form.on('file', function(name, file) {
        var type = file.headers['content-type'];
        type = type.split('/');
        type = type[1];
        fs.rename(file.path, path + 'profile.' + type);
        path = '/images/users/' + req.session.userId + '/profile.' + type;
    });

    form.on('part', function(part) {
        var type = part.headers['content-type'];
        var size = part.byteCount - part.byteOffset;

        if(type != 'image/jpeg' && type != 'image/png' && type != 'image/gif' != 'application/pdf' || size > maxSize)
        {
            this.emit('error');
        }
    });

    form.on('close', function(){
        res.json({err: 0, path: path});
    });

    form.parse(req);

});

Ответ 3

Чтобы прервать загрузку, правильная вещь - закрыть сокет.

req.socket.end();

К сожалению, ситуация, когда сервер хочет прервать выполнение HTTP-загрузки, является беспорядком.

Собственная, совместимая со спецификацией вещь, которую нужно сделать здесь, - просто отправить HTTP 413 response early – то есть, как только вы обнаружите, что клиент отправил больше байтов, чем вы хотите обработать. Это зависит от вас, прекратите ли вы сокет после отправки ответа об ошибке. Это соответствует RFC 2616. [...] Что происходит дальше, не идеально.

  • Если вы оставите сокет открытым, все браузеры (Chrome 30, IE 10, Firefox 21) будут продолжать отправлять данные до тех пор, пока весь файл не будет загружен. Затем и только тогда браузер отобразит ваше сообщение об ошибке. Это действительно отстой, поскольку пользователь должен дождаться завершения всего файла, только чтобы узнать, что сервер отклонил его. Он также снижает вашу пропускную способность.

    Текущее поведение браузеров является нарушением RFC 2616 § 8.2.2:

    Клиент HTTP/1.1 (или более поздний), отправляющий тело сообщения, ДОЛЖЕН отслеживать сетевое подключение для состояния ошибки, пока он передает запрос. Если клиент видит состояние ошибки, он ДОЛЖЕН немедленно прекратить передачу тела. Если тело отправляется с использованием "закодированного" кодирования (раздел 3.6), то для того, чтобы преждевременно пометить конец сообщения, можно использовать кусок нулевой длины и пустой трейлер. Если телу предшествовал заголовок Content-Length, клиент ДОЛЖЕН закрыть соединение.

    Есть открыть Chrome и Firefox, но не ожидайте исправления в ближайшее время.

  • Если вы сразу же закрываете сокет после отправки ответа HTTP 413, все браузеры, очевидно, перестанут загружаться сразу, но в настоящее время они отображают ошибку "connection reset" (или аналогичную), а не любой HTML-код, который вы можете отправить в ответе.

    Опять же, это, вероятно, является нарушением спецификации (что позволяет серверу отправлять ответ раньше и закрывать соединение), но я не ожидал, что исправления браузера в ближайшее время тоже будут.

Тот факт, что вы видите цикл запросов POST, является подозрительным. Вы используете какой-то загрузчик AJAX? Это может быть автоматическое повторение загрузки после закрытия сокета раньше.

Ответ 4

Form.pause фактически предотвратит отправку браузером большего количества данных. Функция вызова ниже сделала это для меня:

var closeConnection = function(res, form){
    try{
    if (!res.ended){
        form.onPart=null;
        form.pause();
        res.end();
        res.ended = true;
    }
    }catch(e){console.log(e);}
}

Ответ 5

Я попытался с вашим решением, похоже, что он не работал на моей машине. Клиент не смог получить ответ после того, как я разместил эту строку this.emit('error'); в разделе form.on. Казалось, моя программа была заблокирована. это означало, что вы не могли дважды вызвать свой метод загрузки. второй вызов не был выполнен, так как ваша программа была заблокирована этой строкой: this.emit('error'); уже.

upload:function(req, res) {
            var form = new multiparty.Form();
            form.on('error', function(message) {
                res.send('Not Okay');
            });
            form.on('part', function(part) {
                console.log('enter form on');
                this.emit('Error');
                res.send('Not Okay');
            });
            form.on('close', function(){
                res.send('Not Okay');
            });
            form.parse(req);
    }

Единственное решение, которое я нашел, это вызов part.resume(); в разделе form.on. он может потреблять ваш файл загрузки на вашем сервере, не касаясь вашего диска. но он будет потреблять ваши сетевые и серверные ресурсы. Во всяком случае, это намного лучше, чем программа была заблокирована. Вот почему я продолжаю искать другой лучший способ его решения.

Ответ 6

Вот мое решение:

var maxSize = 30 * 1024 * 1024;    //30MB
app.post('/upload', function(req, res) {

    var size = req.headers['content-length'];
    if (size <= maxSize) {
        form.parse(req, function(err, fields, files) {
            console.log("File uploading");
            if (files && files.upload) {
                res.status(200).json({fields: fields, files: files});
                fs.renameSync(files.upload[0].path, uploadDir + files.upload[0].originalFilename);
            }
            else {
              res.send("Not uploading");
            }
        });
    }
    else {
        res.send(413, "File to large");
    }

И в случае потери времени загрузки клиента перед получением ответа, управляйте им в javascript клиента.

if (fileElement.files[0].size > maxSize) {
    ....
}

Ответ 7

Вот решение, которое работает для меня с использованием многопартийности.

Ответ, заданный jfizz, не работает во всех браузерах.

ОЧЕНЬ ВАЖНО: Как упоминалось выше в josh3736 в комментарии, вы ДОЛЖНЫ установить закрытие заголовка соединения

form.on('part', function(part) {
  if (![some condition I want to be met]) {
    form.emit('error', new Error('File upload canceled by the server.'));
    return;
  }
  ... code to handle the uploading process normally ...
});

form.on('error', function(err) {
  console.log('ERROR uploading the file:',err.message);
  res.header('Connection', 'close');
  res.status(400).send({ status:'error' });
  setTimeout(function() { res.end(); }, 500);
  next(err);
});