Jak przesłać plik z metadanymi przy użyciu usługi sieci Web REST?


250

Mam usługę sieci Web REST, która obecnie udostępnia ten adres URL:

http: // serwer / dane / media

gdzie użytkownicy mogą wykonać POSTnastępujący JSON:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

w celu utworzenia nowych metadanych mediów.

Teraz potrzebuję możliwości przesłania pliku w tym samym czasie, co metadane multimedialne. Jak najlepiej to zrobić? Mógłbym wprowadzić nową właściwość o nazwie filei kodowanie base64 pliku, ale zastanawiałem się, czy istnieje lepszy sposób.

Jest też multipart/form-datacoś w rodzaju wysyłania formularza HTML, ale korzystam z usługi internetowej REST i chcę w miarę możliwości trzymać się JSON.


36
Trzymanie się tylko JSON nie jest tak naprawdę wymagane, aby mieć usługę RESTful. REST to w zasadzie wszystko, co jest zgodne z głównymi zasadami metod HTTP i innymi (prawdopodobnie niestandardowymi) regułami.
Erik Kaplun,

Odpowiedzi:


192

Zgadzam się z Gregiem, że podejście dwufazowe jest rozsądnym rozwiązaniem, ale zrobiłbym to na odwrót. Chciałbym zrobić:

POST http://server/data/media
body:
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

Aby utworzyć pozycję metadanych i zwrócić odpowiedź, taką jak:

201 Created
Location: http://server/data/media/21323
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentUrl": "http://server/data/media/21323/content"
}

Klient może następnie użyć tego ContentUrl i wykonać PUT z danymi pliku.

Zaletą tego podejścia jest to, że kiedy serwer zaczyna obciążać się ogromnymi ilościami danych, zwracany adres URL może po prostu wskazywać inny serwer z większą przestrzenią / pojemnością. Lub możesz zastosować jakieś okrągłe podejście robin, jeśli problem stanowi przepustowość.


8
Jedną z zalet wysyłania najpierw treści jest to, że do czasu istnienia metadanych treść jest już obecna. Ostatecznie właściwa odpowiedź zależy od organizacji danych w systemie.
Greg Hewgill

Dzięki, zaznaczyłem to jako poprawną odpowiedź, ponieważ właśnie to chciałem zrobić. Niestety z powodu dziwnej reguły biznesowej musimy zezwolić na przesyłanie w dowolnej kolejności (najpierw metadane lub najpierw plik). Zastanawiałem się, czy istnieje sposób na połączenie tych dwóch, aby zaoszczędzić sobie bólu głowy związanego z radzeniem sobie w obu sytuacjach.
Daniel T.

@Daniel Jeśli najpierw wyślesz plik danych, możesz pobrać adres URL zwrócony w lokalizacji i dodać go do atrybutu ContentUrl w metadanych. W ten sposób, gdy serwer odbierze metadane, jeśli istnieje ContentUrl, wtedy już wie, gdzie jest plik. Jeśli nie ma ContentUrl, to wie, że powinien je utworzyć.
Darrel Miller

gdybyś najpierw zrobił POST, czy opublikowałbyś pod tym samym adresem URL? (/ server / data / media) czy stworzyłbyś inny punkt wejścia do przesyłania plików w pierwszej kolejności?
Matt Brailsford,

1
@ Faraway Co zrobić, jeśli metadane zawierają liczbę „polubień” obrazu? Czy traktowałbyś to jako pojedynczy zasób? Lub, co bardziej oczywiste, sugerujesz, że jeśli chciałbym edytować opis obrazu, musiałbym ponownie załadować obraz? W wielu przypadkach formularze wieloczęściowe są właściwym rozwiązaniem. Po prostu nie zawsze tak jest.
Darrel Miller

104

To, że nie pakujesz całego ciała żądania w JSON, nie oznacza, że ​​RESTful nie może używać multipart/form-datazarówno JSON, jak i plików w jednym żądaniu:

curl -F "metadata=<metadata.json" -F "file=@my-file.tar.gz" http://example.com/add-file

po stronie serwera (używając Pythona dla pseudokodu):

class AddFileResource(Resource):
    def render_POST(self, request):
        metadata = json.loads(request.args['metadata'][0])
        file_body = request.args['file'][0]
        ...

aby przesłać wiele plików, można użyć osobnych „pól formularza” dla każdego:

curl -F "metadata=<metadata.json" -F "file1=@some-file.tar.gz" -F "file2=@some-other-file.tar.gz" http://example.com/add-file

... w takim przypadku kod serwera będzie miał request.args['file1'][0]irequest.args['file2'][0]

lub użyj tego samego dla wielu:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz" -F "files=@some-other-file.tar.gz" http://example.com/add-file

... w takim przypadku request.args['files']będzie to po prostu lista długości 2.

lub prześlij wiele plików przez jedno pole:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz,some-other-file.tar.gz" http://example.com/add-file

...w którym to przypadku request.args['files'] będzie ciąg znaków zawierający wszystkie pliki, które będziesz musiał sam przeanalizować - nie wiem, jak to zrobić, ale jestem pewien, że nie jest to trudne, albo lepiej po prostu użyj poprzednich podejść.

Różnica między @i <polega na tym, @że plik zostaje dołączony jako przesyłany plik, a <zawartość pliku jest dołączana jako pole tekstowe.

PS To, że używam curljako sposobu generowania POSTżądań, nie oznacza, że ​​dokładnie takie same żądania HTTP nie mogą być wysyłane z języka programowania, takiego jak Python, ani za pomocą jakiegokolwiek wystarczająco sprawnego narzędzia.


4
Zastanawiałem się nad tym podejściem i dlaczego jeszcze nie widziałem, żeby ktoś to przedstawił. Zgadzam się, wydaje mi się całkowicie ODPOCZONY.
soupdog

1
TAK! Jest to bardzo praktyczne podejście i nie jest mniej RESTfuling niż użycie „application / json” jako typu zawartości dla całego żądania.
sickill

.. ale jest to możliwe tylko wtedy, gdy masz dane w pliku .json i prześlesz je, co nie jest prawdą
itsjavi

5
@mjolnic twój komentarz jest nieistotny: przykłady cURL są po prostu przykładami ; odpowiedź wyraźnie stwierdza, że ​​możesz użyć czegokolwiek, aby wysłać prośbę ... także, co uniemożliwia ci pisanie curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun,

3
Korzystam z tego podejścia, ponieważ zaakceptowana odpowiedź nie działałaby dla opracowywanej aplikacji (plik nie może istnieć przed danymi i dodaje niepotrzebnej złożoności do obsługi przypadku, w którym dane są przesyłane jako pierwsze, a plik nigdy nie jest przesyłany) .
BitsEvolved

33

Jednym ze sposobów rozwiązania tego problemu jest uczynienie przesyłania dwufazowym procesem. Najpierw sam prześlesz plik za pomocą testu POST, w którym serwer zwraca klientowi pewien identyfikator (identyfikatorem może być SHA1 zawartości pliku). Następnie drugie żądanie wiąże metadane z danymi pliku:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentID": "7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}

Włączenie pliku bazy danych64 zakodowanego w samym żądaniu JSON zwiększy rozmiar przesyłanych danych o 33%. To może, ale nie musi być ważne, w zależności od ogólnego rozmiaru pliku.

Innym podejściem może być użycie testu POST nieprzetworzonych danych pliku, ale dołączenie dowolnych metadanych do nagłówka żądania HTTP. Jest to jednak nieco poza podstawowymi operacjami REST i może być bardziej niezręczne w przypadku niektórych bibliotek klienta HTTP.


Możesz użyć Ascii85 zwiększając się tylko o 1/4.
Singagirl

Wszelkie odniesienia do tego, dlaczego base64 tak bardzo zwiększa rozmiar?
jam01

1
@ jam01: Przypadkowo, właśnie widziałem wczoraj coś, co dobrze odpowiada na pytanie dotyczące przestrzeni: jaka jest przestrzeń związana z kodowaniem Base64?
Greg Hewgill,

10

Zdaję sobie sprawę, że to bardzo stare pytanie, ale mam nadzieję, że pomoże to komuś innemu, gdy natknąłem się na ten post w poszukiwaniu tego samego. Miałem podobny problem, tyle że moje metadane to Guid i int. Rozwiązanie jest takie samo. Możesz po prostu uczynić potrzebne metadane częścią adresu URL.

Metoda akceptacji POST w twojej klasie „Controller”:

public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude)
{
    //See http://stackoverflow.com/a/10327789/431906 for how to accept a file
    return null;
}

Następnie, niezależnie od tego, co rejestrujesz trasy, w tym przypadku dla mnie WebApiConfig.Register (konfiguracja HttpConfiguration).

config.Routes.MapHttpRoute(
    name: "FooController",
    routeTemplate: "api/{controller}/{name}/{latitude}/{longitude}",
    defaults: new { }
);

6

Jeśli Twój plik i jego metadane tworzą jeden zasób, możesz przesłać je oba w jednym żądaniu. Przykładowe żądanie będzie:

POST https://target.com/myresources/resourcename HTTP/1.1

Accept: application/json

Content-Type: multipart/form-data; 

boundary=-----------------------------28947758029299

Host: target.com

-------------------------------28947758029299

Content-Disposition: form-data; name="application/json"

{"markers": [
        {
            "point":new GLatLng(40.266044,-74.718479), 
            "homeTeam":"Lawrence Library",
            "awayTeam":"LUGip",
            "markerImage":"images/red.png",
            "information": "Linux users group meets second Wednesday of each month.",
            "fixture":"Wednesday 7pm",
            "capacity":"",
            "previousScore":""
        },
        {
            "point":new GLatLng(40.211600,-74.695702),
            "homeTeam":"Hamilton Library",
            "awayTeam":"LUGip HW SIG",
            "markerImage":"images/white.png",
            "information": "Linux users can meet the first Tuesday of the month to work out harward and configuration issues.",
            "fixture":"Tuesday 7pm",
            "capacity":"",
            "tv":""
        },
        {
            "point":new GLatLng(40.294535,-74.682012),
            "homeTeam":"Applebees",
            "awayTeam":"After LUPip Mtg Spot",
            "markerImage":"images/newcastle.png",
            "information": "Some of us go there after the main LUGip meeting, drink brews, and talk.",
            "fixture":"Wednesday whenever",
            "capacity":"2 to 4 pints",
            "tv":""
        },
] }

-------------------------------28947758029299

Content-Disposition: form-data; name="name"; filename="myfilename.pdf"

Content-Type: application/octet-stream

%PDF-1.4
%
2 0 obj
<</Length 57/Filter/FlateDecode>>stream
x+r
26S00SI2P0Qn
F
!i\
)%!Y0i@.k
[
endstream
endobj
4 0 obj
<</Type/Page/MediaBox[0 0 595 842]/Resources<</Font<</F1 1 0 R>>>>/Contents 2 0 R/Parent 3 0 R>>
endobj
1 0 obj
<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>
endobj
3 0 obj
<</Type/Pages/Count 1/Kids[4 0 R]>>
endobj
5 0 obj
<</Type/Catalog/Pages 3 0 R>>
endobj
6 0 obj
<</Producer(iTextSharp 5.5.11 2000-2017 iText Group NV \(AGPL-version\))/CreationDate(D:20170630120636+02'00')/ModDate(D:20170630120636+02'00')>>
endobj
xref
0 7
0000000000 65535 f 
0000000250 00000 n 
0000000015 00000 n 
0000000338 00000 n 
0000000138 00000 n 
0000000389 00000 n 
0000000434 00000 n 
trailer
<</Size 7/Root 5 0 R/Info 6 0 R/ID [<c7c34272c2e618698de73f4e1a65a1b5><c7c34272c2e618698de73f4e1a65a1b5>]>>
%iText-5.5.11
startxref
597
%%EOF

-------------------------------28947758029299--

3

Nie rozumiem, dlaczego w ciągu ośmiu lat nikt nie opublikował łatwej odpowiedzi. Zamiast zakodować plik jako base64, zakoduj plik json jako ciąg. Następnie po prostu odkoduj json po stronie serwera.

W JavaScript:

let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));

POST to używając Content-Type: multipart / form-data

Po stronie serwera pobierz plik normalnie i pobierz plik json jako ciąg. Konwertuj ciąg znaków na obiekt, który zwykle stanowi jeden wiersz kodu, bez względu na używany język programowania.

(Tak, działa świetnie. Robię to w jednej z moich aplikacji).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.