Datos de formulario o multiparte de análisis de HTTP

Función de Cloud Function que analiza las solicitudes de formularios HTTP.

Páginas de documentación que incluyen esta muestra de código

Para ver la muestra de código usada en contexto, consulta la siguiente documentación:

Muestra de código

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace HttpFormData
{
    public class Function : IHttpFunction
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public async Task HandleAsync(HttpContext context)
        {
            HttpResponse response = context.Response;
            HttpRequest request = context.Request;

            if (request.Method != "POST")
            {
                response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
                return;
            }

            // This code will process each file uploaded.
            string tempDirectory = Path.GetTempPath();
            foreach (IFormFile file in request.Form.Files)
            {
                if (string.IsNullOrEmpty(file.FileName))
                {
                    continue;
                }
                _logger.LogInformation("Processed file: {file}", file.FileName);

                // Note: GCF's temp directory is an in-memory file system
                // Thus, any files in it must fit in the instance's memory.
                string outputPath = Path.Combine(tempDirectory, file.FileName);

                // Note: files saved to a GCF instance itself may not persist across executions.
                // Persistent files should be stored elsewhere, e.g. a Cloud Storage bucket.
                using (FileStream output = File.Create(outputPath))
                {
                    await file.CopyToAsync(output);
                }

                // TODO(developer): process saved files here
                File.Delete(outputPath);
            }

            // This code will process other form fields.
            foreach (KeyValuePair<string, StringValues> parameter in request.Form)
            {
                // TODO(developer): process field values here
                _logger.LogInformation("Processed field '{key}' (value: '{value}')",
                    parameter.Key, (string) parameter.Value);
            }
        }
    }
}

C++

#include <google/cloud/functions/http_request.h>
#include <google/cloud/functions/http_response.h>
#include <absl/strings/str_split.h>
#include <absl/strings/string_view.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <functional>
#include <regex>
#include <vector>

namespace gcf = ::google::cloud::functions;

namespace {
class FormDataDelimiter {
 public:
#if defined(__APPLE__) && defined(__clang__)
  using searcher = std::default_searcher<absl::string_view::iterator>;
#else
  using searcher =
      std::boyer_moore_horspool_searcher<absl::string_view::iterator>;
#endif  // ! __APPLE__ && __clang__
  static FormDataDelimiter FromHeader(std::string const& header);
  [[nodiscard]] absl::string_view Find(absl::string_view text,
                                       std::size_t pos) const;

 private:
  explicit FormDataDelimiter(std::string body, std::string part)
      : body_marker_(std::move(body)),
        body_marker_view_(body_marker_),
        body_marker_searcher_(body_marker_view_.begin(),
                              body_marker_view_.end()),
        part_marker_(std::move(part)),
        part_marker_view_(part_marker_),
        part_marker_searcher_(part_marker_view_.begin(),
                              part_marker_view_.end()) {}

  std::string body_marker_;
  absl::string_view body_marker_view_;
  searcher body_marker_searcher_;
  std::string part_marker_;
  absl::string_view part_marker_view_;
  searcher part_marker_searcher_;
};

}  // namespace

gcf::HttpResponse http_form_data(gcf::HttpRequest request) {
  if (request.verb() != "POST") {
    return gcf::HttpResponse{}.set_result(gcf::HttpResponse::kMethodNotAllowed);
  }
  auto const header = request.headers().find("content-type");
  if (header == request.headers().end()) {
    return gcf::HttpResponse{}.set_result(gcf::HttpResponse::kBadRequest);
  }
  auto const& content_type = header->second;
  if (content_type.rfind("multipart/form-data", 0) != 0) {
    return gcf::HttpResponse{}.set_result(gcf::HttpResponse::kBadRequest);
  }
  auto delimiter = FormDataDelimiter::FromHeader(header->second);

  auto payload = std::move(request).payload();
  std::vector<absl::string_view> parts =
      absl::StrSplit(payload, delimiter, absl::SkipEmpty{});
  nlohmann::json result{{"parts", std::vector<nlohmann::json>{}}};
  for (auto& p : parts) {
    std::vector<absl::string_view> components =
        absl::StrSplit(p, absl::MaxSplits("\r\n\r\n", 2));
    auto const body_size =
        components.size() == 2 ? components[0].size() : std::size_t(0);

    std::vector<std::string> part_headers =
        absl::StrSplit(components[0], "\r\n");
    nlohmann::json descriptor{{"bodySize", body_size},
                              {"headerCount", part_headers.size()}};
    for (auto const& h : part_headers) {
      static auto const kContentDispositionRE = std::regex(
          R"re(^content-disposition:)re",
          std::regex::extended | std::regex::icase | std::regex::optimize);
      static auto const kNameRE =
          std::regex(R"re([;[:space:]]name=([^[:space:];]+))re",
                     std::regex::extended | std::regex::icase);
      // NOTE: this does not handle embedded spaces and/or semi-colons in the
      // filename, and so should be improved for production code.
      static auto const kFilenameRE =
          std::regex(R"re([;[:space:]]filename=([^[:space:];]+))re",
                     std::regex::extended | std::regex::icase);

      if (!std::regex_search(h.begin(), h.end(), kContentDispositionRE)) {
        continue;
      }
      std::smatch m;
      if (std::regex_search(h.begin(), h.end(), m, kNameRE)) {
        descriptor["name"] = m[1];
      }

      if (!std::regex_search(h.begin(), h.end(), m, kFilenameRE)) continue;
      descriptor["isFile"] = true;
      descriptor["filename"] = m[1];
    }
    result["parts"].push_back(std::move(descriptor));
  }

  return gcf::HttpResponse{}
      .set_header("content-type", "application/json")
      .set_payload(result.dump());
}

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"fmt"
	"log"
	"net/http"
)

// UploadFile processes a 'multipart/form-data' upload request.
func UploadFile(w http.ResponseWriter, r *http.Request) {
	const maxMemory = 2 * 1024 * 1024 // 2 megabytes.

	// ParseMultipartForm parses a request body as multipart/form-data.
	// The whole request body is parsed and up to a total of maxMemory bytes of
	// its file parts are stored in memory, with the remainder stored on
	// disk in temporary files.

	// Note that any files saved during a particular invocation may not
	// persist after the current invocation completes; persistent files
	// should be stored elsewhere, such as in a Cloud Storage bucket.
	if err := r.ParseMultipartForm(maxMemory); err != nil {
		http.Error(w, "Unable to parse form", http.StatusBadRequest)
		log.Printf("Error parsing form: %v", err)
		return
	}

	// Be sure to remove all temporary files after your function is finished.
	defer func() {
		if err := r.MultipartForm.RemoveAll(); err != nil {
			http.Error(w, "Error cleaning up form files", http.StatusInternalServerError)
			log.Printf("Error cleaning up form files: %v", err)
		}
	}()

	// r.MultipartForm.File contains *multipart.FileHeader values for every
	// file in the form. You can access the file contents using
	// *multipart.FileHeader's Open method.
	for _, headers := range r.MultipartForm.File {
		for _, h := range headers {
			fmt.Fprintf(w, "File uploaded: %q (%v bytes)", h.Filename, h.Size)
			// Use h.Open() to read the contents of the file.
		}
	}

}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.logging.Logger;

public class HttpFormData implements HttpFunction {
  private static final Logger logger = Logger.getLogger(HttpFormData.class.getName());

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {

    if (!"POST".equals(request.getMethod())) {
      response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
      return;
    }

    // This code will process each file uploaded.
    String tempDirectory = System.getProperty("java.io.tmpdir");
    for (HttpRequest.HttpPart httpPart : request.getParts().values()) {
      String filename = httpPart.getFileName().orElse(null);
      if (filename == null) {
        continue;
      }

      logger.info("Processed file: " + filename);

      // Note: GCF's temp directory is an in-memory file system
      // Thus, any files in it must fit in the instance's memory.
      Path filePath = Paths.get(tempDirectory, filename).toAbsolutePath();

      // Note: files saved to a GCF instance itself may not persist across executions.
      // Persistent files should be stored elsewhere, e.g. a Cloud Storage bucket.
      Files.copy(httpPart.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);

      // TODO(developer): process saved files here
      Files.delete(filePath);
    }

    // This code will process other form fields.
    request.getQueryParameters().forEach(
        (fieldName, fieldValues) -> {
          String firstFieldValue = fieldValues.get(0);

          // TODO(developer): process field values here
          logger.info(String.format(
              "Processed field: %s (value: %s)", fieldName, firstFieldValue));
        });
  }
}

Node.js

/**
 * Parses a 'multipart/form-data' upload request
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
const path = require('path');
const os = require('os');
const fs = require('fs');

// Node.js doesn't have a built-in multipart/form-data parsing library.
// Instead, we can use the 'busboy' library from NPM to parse these requests.
const Busboy = require('busboy');

exports.uploadFile = (req, res) => {
  if (req.method !== 'POST') {
    // Return a "method not allowed" error
    return res.status(405).end();
  }
  const busboy = new Busboy({headers: req.headers});
  const tmpdir = os.tmpdir();

  // This object will accumulate all the fields, keyed by their name
  const fields = {};

  // This object will accumulate all the uploaded files, keyed by their name.
  const uploads = {};

  // This code will process each non-file field in the form.
  busboy.on('field', (fieldname, val) => {
    /**
     *  TODO(developer): Process submitted field values here
     */
    console.log(`Processed field ${fieldname}: ${val}.`);
    fields[fieldname] = val;
  });

  const fileWrites = [];

  // This code will process each file uploaded.
  busboy.on('file', (fieldname, file, filename) => {
    // Note: os.tmpdir() points to an in-memory file system on GCF
    // Thus, any files in it must fit in the instance's memory.
    console.log(`Processed file ${filename}`);
    const filepath = path.join(tmpdir, filename);
    uploads[fieldname] = filepath;

    const writeStream = fs.createWriteStream(filepath);
    file.pipe(writeStream);

    // File was processed by Busboy; wait for it to be written.
    // Note: GCF may not persist saved files across invocations.
    // Persistent files must be kept in other locations
    // (such as Cloud Storage buckets).
    const promise = new Promise((resolve, reject) => {
      file.on('end', () => {
        writeStream.end();
      });
      writeStream.on('finish', resolve);
      writeStream.on('error', reject);
    });
    fileWrites.push(promise);
  });

  // Triggered once all uploaded files are processed by Busboy.
  // We still need to wait for the disk writes (saves) to complete.
  busboy.on('finish', async () => {
    await Promise.all(fileWrites);

    /**
     * TODO(developer): Process saved files here
     */
    for (const file in uploads) {
      fs.unlinkSync(uploads[file]);
    }
    res.send();
  });

  busboy.end(req.rawBody);
};

PHP


use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Psr7\Response;

function uploadFile(ServerRequestInterface $request): ResponseInterface
{
    if ($request->getMethod() != 'POST') {
        return new Response(405, [], 'Method Not Allowed: expected POST, found ' . $request->getMethod());
    }

    $contentType = $request->getHeader('Content-Type')[0];
    if (strpos($contentType, 'multipart/form-data') !== 0) {
        return new Response(400, [], 'Bad Request: content of type "multipart/form-data" not provided, found ' . $contentType);
    }

    $fileList = [];
    /** @var $file Psr\Http\Message\UploadedFileInterface */
    foreach ($request->getUploadedFiles() as $name => $file) {
        // Use caution when trusting the client-provided filename:
        // https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload
        $fileList[] = $file->getClientFilename();

        infoLog('Processing ' . $file->getClientFilename());
        $filename = tempnam(sys_get_temp_dir(), $name . '.') . '-' . $file->getClientFilename();

        // Use $file->getStream() to process the file contents in ways other than a direct "file save".
        infoLog('Saving to ' . $filename);
        $file->moveTo($filename);
    }

    if (empty($fileList)) {
        $msg = 'Bad Request: no files sent for upload';
        errorLog($msg);
        return new Response(400, [], $msg);
    }

    return new Response(201, [], 'Saved ' . join(', ', $fileList));
}

function errorLog($msg): void
{
    $stream = fopen('php://stderr', 'wb');
    $entry = json_encode(['msg' => $msg, 'severity' => 'error'], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    fwrite($stream, $entry . PHP_EOL);
}

function infoLog($msg): void
{
    $stream = fopen('php://stderr', 'wb');
    $entry = json_encode(['message' => $msg, 'severity' => 'info'], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    fwrite($stream, $entry . PHP_EOL);
}

Python

import os
import tempfile
from werkzeug.utils import secure_filename

# Helper function that computes the filepath to save files to
def get_file_path(filename):
    # Note: tempfile.gettempdir() points to an in-memory file system
    # on GCF. Thus, any files in it must fit in the instance's memory.
    file_name = secure_filename(filename)
    return os.path.join(tempfile.gettempdir(), file_name)

def parse_multipart(request):
    """ Parses a 'multipart/form-data' upload request
    Args:
        request (flask.Request): The request object.
    Returns:
        The response text, or any set of values that can be turned into a
         Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    # This code will process each non-file field in the form
    fields = {}
    data = request.form.to_dict()
    for field in data:
        fields[field] = data[field]
        print('Processed field: %s' % field)

    # This code will process each file uploaded
    files = request.files.to_dict()
    for file_name, file in files.items():
        # Note: GCF may not keep files saved locally between invocations.
        # If you want to preserve the uploaded files, you should save them
        # to another location (such as a Cloud Storage bucket).
        file.save(get_file_path(file_name))
        print('Processed file: %s' % file_name)

    # Clear temporary directory
    for file_name in files:
        file_path = get_file_path(file_name)
        os.remove(file_path)

    return "Done!"

Ruby

require "functions_framework"

FunctionsFramework.http "http_form_data" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request

  # This Rack call parses multipart form data, returning the params as a hash.
  # The returned params hash includes an entry for each field. File uploads
  # are written to Tempfiles and represented by hashes, while plain fields are
  # represented by strings.
  params = request.POST

  begin
    params.each do |name, part|
      if part.is_a? Hash
        # Handle a file upload part by logging the md5 hash.
        md5 = Digest::MD5.hexdigest part[:tempfile].read
        file_name = part[:filename]
        logger.info "Processed file=#{file_name} md5=#{md5}"
      else
        # Handle a non-file part by logging the value.
        logger.info "Processed field=#{name} value=#{part}"
      end
    end
  ensure
    # Ensure that all Tempfile objects are closed and deleted. The Cloud
    # Functions runtime keeps temporary files in an in-memory file system,
    # so to lower memory usage it is good practice to clean up Tempfiles
    # explicitly rather than wait for object finalization.
    params.each_value do |part|
      part[:tempfile].close! if part.is_a? Hash
    end
  end

  # The HTTP response body.
  "OK"
end

¿Qué sigue?

Para buscar y filtrar muestras de código para otros productos de Google Cloud, consulta el navegador de muestra de Google Cloud.