API REST - processamento de arquivos (ou seja, imagens) - práticas recomendadas

194

Estamos desenvolvendo servidor com API REST, que aceita e responde com JSON. O problema é que, se você precisar fazer upload de imagens do cliente para o servidor.

Nota: e também estou falando de um caso de uso em que a entidade (usuário) pode ter vários arquivos (carPhoto, licensePhoto) e também ter outras propriedades (nome, email ...), mas quando você cria um novo usuário, você não enviar essas imagens, elas são adicionadas após o processo de registro.


As soluções que eu conheço, mas cada uma delas tem algumas falhas

1. Use multipart / form-data em vez de JSON

good : as solicitações POST e PUT são o mais RESTful possível, elas podem conter entradas de texto junto com o arquivo.

contras : não é mais JSON, o que é muito mais fácil de testar, depurar etc. comparar com dados de várias partes / formulário

2. Permitir atualizar arquivos separados

A solicitação POST para criar novo usuário não permite adicionar imagens (o que é bom em nosso caso de uso, como eu disse no início), o upload de imagens é feito pela solicitação PUT como multipart / form-data para, por exemplo / users / 4 / carPhoto

good : tudo (exceto o arquivo que está sendo carregado) permanece em JSON, é fácil testar e depurar (você pode registrar solicitações JSON completas sem ter medo do tamanho)

contras : Não é intuitivo, você não pode POST ou PUT todas as variáveis ​​da entidade de uma só vez e também esse endereço /users/4/carPhotopode ser considerado mais uma coleção (o caso de uso padrão da API REST se parece com isso /users/4/shipments). Normalmente você não pode (e não quer) GET / PUT cada variável da entidade, por exemplo users / 4 / name. Você pode obter um nome com GET e alterá-lo com PUT em users / 4. Se houver algo após o ID, geralmente haverá outra coleção, como users / 4 / reviews

3. Use Base64

Envie-o como JSON, mas codifique arquivos com o Base64.

good : Igual à primeira solução, é o serviço RESTful possível.

contras : Mais uma vez, o teste e a depuração são muito piores (o corpo pode ter megabytes de dados), há um aumento no tamanho e também no tempo de processamento em ambos - cliente e servidor


Eu realmente gostaria de usar a solução não. 2, mas tem seus contras ... Qualquer pessoa pode me dar uma idéia melhor da solução "o que é melhor"?

Meu objetivo é ter serviços RESTful com o máximo possível de padrões incluídos, enquanto eu quero mantê-lo o mais simples possível.

libik
fonte
Você também pode achar isso útil: stackoverflow.com/questions/4083702/…
Markon 29/10/2015
5
Sei que esse tópico é antigo, mas enfrentamos esse problema recentemente. A melhor abordagem que temos é semelhante ao seu número 2. Carregamos arquivos diretamente para a API e, em seguida, anexamos esses arquivos no modelo. Com esse cenário, você pode criar imagens de upload antes, depois ou na mesma página do formulário, realmente não importa. Boa discussão!
Tiago Matos
2
@TiagoMatos - sim, exatamente, eu o descrevi em uma resposta que aceitei recentemente #
libik
6
Obrigado por fazer esta pergunta.
Zuhayer Tahir
1
"também este endereço / users / 4 / carPhoto pode ser considerado mais uma coleção" - não, não se parece com uma coleção e não seria necessariamente considerado uma coleção. É totalmente bom ter uma relação com um recurso que não é uma coleção, mas um recurso único.
B12Toaster 15/05

Respostas:

152

OP aqui (estou respondendo a essa pergunta após dois anos, o post feito por Daniel Cerecedo não era ruim de uma vez, mas os serviços da web estão se desenvolvendo muito rápido)

Após três anos de desenvolvimento de software em tempo integral (com foco também na arquitetura de software, gerenciamento de projetos e arquitetura de microsserviços), eu definitivamente escolho a segunda maneira (mas com um ponto de extremidade geral) como a melhor.

Se você possui um ponto final especial para imagens, ele oferece muito mais poder sobre o manuseio dessas imagens.

Temos a mesma API REST (Node.js) para aplicativos móveis (iOS / android) e front-end (usando o React). Este é o ano de 2017, portanto, você não deseja armazenar imagens localmente, deseja enviá-las para algum armazenamento na nuvem (Google cloud, s3, cloudinary, ...); portanto, deseja um tratamento geral sobre elas.

Nosso fluxo típico é que, assim que você seleciona uma imagem, ela começa a ser carregada em segundo plano (geralmente POST no terminal / imagens), retornando o ID após o upload. Isso é realmente fácil de usar, porque o usuário escolhe uma imagem e, em seguida, geralmente prossegue com outros campos (por exemplo, endereço, nome, ...); portanto, quando ele pressiona o botão "enviar", a imagem já está carregada. Ele não espera e vê a tela dizendo "enviando ...".

O mesmo vale para obter imagens. Especialmente graças aos telefones celulares e aos dados móveis limitados, você não deseja enviar imagens originais, deseja redimensionar imagens, para que elas não ocupem tanta largura de banda (e para tornar seus aplicativos móveis mais rápidos, muitas vezes você não deseja para redimensioná-lo, você deseja a imagem que se encaixa perfeitamente na sua exibição). Por esse motivo, bons aplicativos estão usando algo como cloudinary (ou temos nosso próprio servidor de imagem para redimensionar).

Além disso, se os dados não forem privados, você envia de volta ao URL do aplicativo / front-end e o baixa diretamente do armazenamento na nuvem, o que é uma enorme economia de largura de banda e tempo de processamento para o servidor. Em nossos aplicativos maiores, há muitos terabytes baixados todos os meses; você não deseja lidar com isso diretamente em cada servidor da API REST, focado na operação de CRUD. Você deseja lidar com isso em um único local (nosso servidor de imagens, que possui cache etc.) ou permitir que os serviços em nuvem lidem com tudo isso.


Contras: Os únicos "contras" em que você deve pensar são "imagens não atribuídas". O usuário seleciona imagens e continua preenchendo outros campos, mas ele diz "nah" e desativa o aplicativo ou a guia, mas, enquanto isso, você carregou a imagem com sucesso. Isso significa que você enviou uma imagem que não foi atribuída a nenhum lugar.

Existem várias maneiras de lidar com isso. O mais fácil é "Não me importo", o que é relevante, se isso não acontecer com muita frequência ou você desejar armazenar todos os usuários de imagens que você enviar (por qualquer motivo) e você não desejar eliminação.

Outra também é fácil - você tem CRON e, a cada semana, e exclui todas as imagens não atribuídas com mais de uma semana.

libik
fonte
O que acontecerá se [assim que você selecionar a imagem, ele iniciará o upload em segundo plano (geralmente POST no terminal / imagens), retornando o ID após o upload] quando a solicitação falhar devido à conexão à Internet? Você solicitará ao usuário enquanto ele prossegue com outros campos (por exemplo, endereço, nome, ...)? Aposto que você ainda esperará até que o usuário aperte o botão "enviar" e tente novamente sua solicitação, fazendo-os esperar enquanto assistia à tela dizendo "uploadiing ...".
Adromil Balais
5
@AdromilBalais - A API RESTful não possui estado, portanto não faz nada (o servidor não rastreia o estado do consumidor). O consumidor do serviço (por exemplo, página da web ou dispositivo móvel) é responsável pelo tratamento de solicitações com falha, portanto, o consumidor deve decidir se chama imediatamente a mesma solicitação após a falha ou o que fazer (por exemplo, mostrar a mensagem "O upload da imagem falhou - deseja tentar novamente ")
libik 18/08/19
2
Resposta muito informativa e esclarecedora. Obrigado por responder.
Zuhayer Tahir
Isso realmente não resolve o problema inicial. Este apenas diz "usar um serviço de nuvem"
Martin Muzatko
3
@MartinMuzatko - escolhe, escolhe a segunda opção e diz como você deve usá-lo e por quê. Se você quer dizer "mas essa não é a opção perfeita que permite enviar tudo em uma solicitação e sem implicação" - sim, infelizmente não existe essa solução.
libik
103

Existem várias decisões a serem tomadas :

  1. O primeiro sobre o caminho do recurso :

    • Modele a imagem como um recurso por si só:

      • Aninhado no usuário (/ user /: id / image): o relacionamento entre o usuário e a imagem é feito implicitamente

      • No caminho raiz (/ image):

        • O cliente é responsável por estabelecer o relacionamento entre a imagem e o usuário, ou;

        • Se um contexto de segurança estiver sendo fornecido com a solicitação POST usada para criar uma imagem, o servidor poderá estabelecer implicitamente um relacionamento entre o usuário autenticado e a imagem.

    • Incorporar a imagem como parte do usuário

  2. A segunda decisão é sobre como representar o recurso de imagem :

    • Como carga JSON codificada em Base 64
    • Como carga útil multipartes

Esta seria a minha pista de decisão:

  • Eu geralmente prefiro o design do que o desempenho, a menos que haja um argumento forte para isso. Torna o sistema mais sustentável e pode ser mais facilmente compreendido pelos integradores.
  • Então, meu primeiro pensamento é optar por uma representação Base64 do recurso de imagem, pois permite manter tudo JSON. Se você escolher esta opção, poderá modelar o caminho do recurso como desejar.
    • Se o relacionamento entre usuário e imagem for de 1 para 1, eu preferiria modelar a imagem como um atributo, especialmente se os dois conjuntos de dados forem atualizados ao mesmo tempo. Em qualquer outro caso, você pode escolher livremente modelar a imagem como um atributo, atualizando-a via PUT ou PATCH ou como um recurso separado.
  • Se você escolher a carga útil de várias partes, eu me sentiria compelido a modelar a imagem como um recurso por si só, para que outros recursos, no nosso caso, o recurso do usuário, não sejam afetados pela decisão de usar uma representação binária para a imagem.

Depois vem a pergunta: Existe algum impacto no desempenho sobre a escolha de base64 versus multipart? . Poderíamos pensar que a troca de dados no formato multipartes deveria ser mais eficiente. Mas este artigo mostra quão pouco as duas representações diferem em termos de tamanho.

Minha escolha Base64:

  • Decisão de design consistente
  • Impacto negligenciável no desempenho
  • Como os navegadores entendem os URIs de dados (imagens codificadas em base64), não há necessidade de transformá-los se o cliente for um navegador
  • Não votarei se o terei como atributo ou recurso autônomo, depende do domínio do problema (que não conheço) e de sua preferência pessoal.
Daniel Cerecedo
fonte
3
Não podemos codificar os dados usando outros protocolos de serialização como protobuf, etc? Basicamente, estou tentando entender se existem outras maneiras mais simples de resolver o tamanho e o aumento do tempo de processamento que acompanham a codificação base64.
Andy Dufresne
1
Resposta muito envolvente. obrigado pela abordagem passo a passo. Isso me fez entender muito melhor seus pontos de vista.
Zuhayer Tahir
13

Sua segunda solução é provavelmente a mais correta. Você deve usar a especificação HTTP e os tipos MIME da maneira como foram planejados e fazer upload do arquivo via multipart/form-data. Quanto a lidar com os relacionamentos, eu usaria esse processo (tendo em mente que não sei nada sobre suas suposições ou design do sistema):

  1. POSTpara /userscriar a entidade do usuário.
  2. POSTa imagem para /images, certificando-se de retornar um Locationcabeçalho para onde a imagem pode ser recuperada de acordo com a especificação HTTP.
  3. PATCHpara /users/carPhotoe atribuí-lo a identificação da foto dada no Locationcabeçalho do passo 2.
mmcclannahan
fonte
1
Eu não tenho nenhum controle direto de "como cliente usará API" ... O problema disso é que "mortos" imagens que não são corrigidos com alguns recursos ...
libik
4
Geralmente, quando você escolhe a segunda opção, é preferível fazer o upload primeiro do elemento de mídia e retornar o identificador de mídia para o cliente, para que o cliente possa enviar os dados da entidade, incluindo o identificador de mídia, essa abordagem evita que as entidades quebradas ou as informações de incompatibilidade.
Kellerman Rivero
2

Não há solução fácil. Cada caminho tem seus prós e contras. Mas a maneira canônica está usando a primeira opção:multipart/form-data . Como o guia de recomendação do W3 diz

O tipo de conteúdo "dados de várias partes / formulário" deve ser usado para enviar formulários que contêm arquivos, dados não ASCII e dados binários.

Na verdade, não estamos enviando formulários, mas o princípio implícito ainda se aplica. Usar base64 como uma representação binária está incorreto porque você está usando a ferramenta incorreta para atingir seu objetivo. Por outro lado, a segunda opção força seus clientes da API a realizar mais tarefas para consumir seu serviço de API. Você deve fazer o trabalho duro no lado do servidor para fornecer uma API fácil de consumir. A primeira opção não é fácil de depurar, mas quando você faz isso, provavelmente nunca muda.

Usando multipart/form-datavocê está preso à filosofia REST / http. Você pode ver uma resposta para uma pergunta semelhante aqui .

Outra opção, se você combinar as alternativas, poderá usar dados de várias partes / formulário, mas, em vez de enviar todos os valores separadamente, poderá enviar um valor denominado payload com a json payload dentro dele. (Tentei essa abordagem usando o ASP.NET WebAPI 2 e funciona bem).

Kellerman Rivero
fonte
2
Esse guia de recomendação do W3 é irrelevante aqui, pois está no contexto da especificação do HTML 4.
Johann
1
Muito verdadeiro .... "dados não ASCII" requerem multipartes? No século XXI? Em um mundo UTF-8? Claro que essa é uma recomendação ridícula para hoje. Estou até surpreso que existisse no HTML 4 dias, mas às vezes o mundo da infraestrutura da Internet se move muito lentamente.
quer