공통 [REST API] REST 기반 파일 업로드와 다운로드 구현하기
2020.08.31 15:27
이 글에서는 REST API 기반 파일 업로드와 다운로드 구현방안을 설명합니다.
REST 서버와 REST 클라이언트를 이용해 기능을 구현했습니다.
REST 기반 파일 업로드와 다운로드 구현
REST API 구현 시 파일을 제공해야하는 경우가 있습니다. 파일 업로드 시 기존의 데이터와 함께 파일을 업로드할 수도 있고, 별도의 파일 전용 엔드포인트를 추가해 구현할 수 있습니다. 이 두가지 방법 모두에 대해 설명합니다.
이 글에 앞서 다음 내용을 이해하고 있어야 합니다. 미리 선행 학습이 필요합니다.
- [REST API] REST API 이해하기
- [REST API][실습] REST API 서버 개발하기(엔드포인트 구현, RAD 서버 이용)
- [REST API][실습] REST API 클라이언트 개발하기(REST Client 이용)
이 글에서는 다음 내용을 다룹니다.
- 파일 엔드포인트 추가 구성
- 파일 업로드 구현 방안
- 서버 측 구현
- 클라이언트 측 구현
- 파일 다운로드 구현 방안
- 서버 측 구현
- 클라이언트 측 구현
파일 엔드포인트 추가 구성
파일을 제공하는 기능을 추가하기 위해서는, 1) 기존 엔드포인트에서 파일 항목을 추가하는 방법과 2) 별도의 엔드포인트를 추가하는 방법으로 구현할 수 있습니다.
이 글에서는 별도의 엔드포인트를 추가해 파일 업로드와 다운로드 기능을 구현하는 방법을 설명합니다.
저는 images라는 리소스이름으로 RAD 서버 패키지 프로젝트를 생성했습니다.
1, File > New > Other
2, RAD Server > RAD Server Package
3, Create package with resource > Next
4, Resource name: images, File type: Data Module > Next
5, 모든 항목 선택 해제 > Finish
다음과 같이 엔드포인트를 추가합니다.(선언부에 추가 후 Ctrl + Shift + C를 눌러 구현부를 자동 생성할 수 있습니다.)
|
1
2
3
4
5
6
7
8
9
|
type [ResourceName('images')] TImagesResource1 = class(TDataModule) published [ResourceSuffix('{item}/photo')] procedure GetItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); [ResourceSuffix('{item}/photo')] procedure PostItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); end; |
엔드포인트는 ResourceSuffix(리소스 접미사)와 메소드로 구성됩니다.
메소드는 Get, Post, Put, Delete 접두사로 시작해야 합니다. 리소스 호출 시 HTTP 메소드와 접두사가 매핑되어 메소드가 호출 됩니다.
즉, GET으로 요청된 경우 Get*으로 시작된 메소드가, POST로 요청한 경우 Post*로 시작된 메소드가 실행됩니다.
ResourceSuffix에 지정된 항목과 매핑된 요청이 온 경우 메소드가 실행됩니다.
중괄호({})로 감싼 부분은 파라미터화된 항목으로 구현부에서 ARequest.Params.Values['item'] 등의 코드로 그부분의 내용을 취득할 수 있습니다.
예를 들어
GET http://localhost:8080/images/1/photo 호출 시 GetItemPhoto 메소드가 호출되고
POST http://localhost:8080/images/1/photo 호출 시 PostItemPhoto 메소드가 호출됩니다.
ARequest.Params.Values['item']은 1을 반환합니다.
파일 업로드 구현 방안
서버(RAS Server) 측 구현
파일 업로드는 PostItemPhoto 메소드에서 구현합니다.
주의할 점은, 파일 전송을 위해서는 multipart/form-data 인코딩 타입으로 데이터가 전달됩니다.
현재(2020년 08월)에 RAD 서버에서 multipart/form-data 타입의 데이터를 처리하는 기능이 구현되어있지 않은 것으로 파악되어, 직접 데이터를 분석해 필요한 데이터를 사용해야 합니다.
저는 다음과 같은 DecodeParamsAndStream 함수를 이용해 데이터를 분석했습니다.
(Indy에서 재공하는 TIdMessageDecoderMIME 객체를 이용했습니다.)
|
1
2
3
4
5
6
7
8
9
10
11
12
|
type TStreamParams = class(TDictionary<string, tstream="">) private function GetStream(const Name: string): TStream; procedure SetStream(const Name: string; const Value: TStream); public property Streams[const Name: string]: TStream read GetStream write SetStream; destructor Destroy; override; end;procedure DecodeParamsAndStream(AStream: TStream; AContentType: string; Params: TStrings; StreamParams: TStreamParams);</string,> |
파라메터로는
- AStream : Body의 전체 스트림
- AContentType : 요청의 컨텐트 타입, boundary 포함
- Params : 문자열 형식의 데이터(파라미터)
- StreamParams : Stream 형식의 데이터(파라미터), TStreamParams 객체는 <string, TStream> 쌍의 딕셔너리 사용
파라미터 데이터들은 컨텐트타입의 boundary 값을 앞뒤에 두어 구분합니다.
주의할 점은 문자열 형식의 파라미터 데이터의 경우 Content-Type이 누락되어 있습니다.(분석 시 누락된 경우에 대해 예외처리가 필요합니다.)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
uses System.IOUtils, IdGlobalProtocols, IdMessageCoder, IdMessageCoderMIME;procedure DecodeParamsAndStream(AStream: TStream; AContentType: string; Params: TStrings; StreamParams: TStreamParams);var Boundary: string; Decoder, NewDecoder: TIdMessageDecoderMIME; MsgEnd: Boolean; FieldName: string; StringStream: TStringStream; MemoryStream: TMemoryStream;begin Boundary := ExtractHeaderSubItem(AContentType, 'boundary', QuoteHTTP); Decoder := TIdMessageDecoderMIME.Create(nil); try MsgEnd := False; repeat Decoder.MIMEBoundary := Boundary; Decoder.SourceStream := AStream; Decoder.FreeSourceStream := False; Decoder.ReadHeader; // Content-Type이 없는 경우 mcptAttachment로 인식해 기본값 설정 // RESTClient에서 MultiPart 전송 시 일반 파라메터의 경우 Content-Type 누락해 전송 함 if Decoder.Headers.Values['Content-Type'] = '' then begin Decoder.Headers.Values['Content-Type'] := 'text/plain'; Decoder.CheckAndSetType(Decoder.Headers.Values['Content-Type'], Decoder.Headers.Values['Content-Disposition']); end; case Decoder.PartType of mcptText: begin FieldName := ExtractHeaderSubItem(Decoder.Headers.Values['Content-Disposition'], 'name', QuoteMIME); StringStream := TStringStream.Create; try NewDecoder := Decoder.ReadBody(StringStream, MsgEnd) as TIdMessageDecoderMIME; try Params.Values[FieldName] := StringStream.DataString.Trim; finally Decoder.Free; Decoder := NewDecoder; end; finally StringStream.Free; end; end; mcptAttachment: begin var HL: string := Decoder.Headers.Values['Content-Disposition']; FieldName := ExtractHeaderSubItem(Decoder.Headers.Values['Content-Disposition'], 'name', QuoteMIME); MemoryStream := TMemoryStream.Create; NewDecoder := Decoder.ReadBody(MemoryStream, MsgEnd) as TIdMessageDecoderMIME; try StreamParams.Streams[FieldName] := MemoryStream; finally Decoder.Free; Decoder := NewDecoder; end; end; mcptIgnore: begin FreeAndNil(Decoder); Decoder := TIdMessageDecoderMIME.Create(nil); TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary; end; mcptEOF: begin FreeAndNil(Decoder); MsgEnd := True; end; end; until (Decoder = nil) or MsgEnd; finally Decoder.Free; end;end; |
위의 코드가 좀 길고 생소할 수 있습니다.
중요한 부분은 디코더로 분석한 데이터의 Decoder.PartType이 mcptText(텍스트)인 경우 Params 항목에 데이터를 추가하고, mcptAttachment(첨부 파일)인 경우 StreamParams 항목에 데이터를 추가했습니다.
위 함수를 사용한 PostItemPhoto 메소드의 코드는 다음과 같습니다.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
const ROOT_PATH = 'D:\Projects\DelphiDemos\OpenAPI\RESTUpload\Server';procedure TImagesResource1.PostItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);var LItem: string; Stream: TStream; ContentType: string; Params: TStringList; StreamParams: TStreamParams; Path, RawPath: string;begin LItem := ARequest.Params.Values['item']; Params := TStringList.Create; StreamParams := TStreamParams.Create; Stream := ARequest.Body.GetStream; ContentType := ARequest.Headers.GetValue('Content-Type'); // e.g. multipart/form-data; boundary=--------070120105641002 Path := TPath.Combine(ROOT_PATH, 'images'); RawPath := TPath.Combine(Path, 'raw_' + LItem + '.jpg'); Path := TPath.Combine(Path, LItem + '.jpg'); // Save raw data TMemoryStream(Stream).SaveToFile(RawPath); // Decode parameters and streams from body stream. DecodeParamsAndStream(Stream, ContentType, Params, StreamParams); // Using parameters as a below if Params.Values['user_id'] = '123' then begin end; // Save photo parameter to a file. if Assigned(StreamParams.Streams['photo']) then TMemoryStream(StreamParams.Streams['photo']).SaveToFile(Path); StreamParams.Free; Params.Free;end; |
참고로, 파일 저장 방식은 파일로 저장하는 방식과 Blob 필드에 저장하는 방식이 있으며, 이 예제에서는 지정경로(ROOT_PATH) 하위 images 디렉토리에 {item}항목 이름으로 저장했습니다.
파일로 저장시의 주의점은 RAD 서버의 로컬 디스크로 저장 시 서버를 병렬화(여러대 구성) 시 접근이 제한됩니다. 네트워크 경로 또는 공유 파일 시스템을 이용해야 합니다.(또는 파일 공유 솔루션 등을 이용할 수도 있습니다.)
Blob 필드로 저장 시 주의점은 데이터와 별도의 테이블에 Blob 필드를 구성하는 것을 추천드립니다. 성능 측면과 향후 데이터 관리(백업 등) 시 유리할 수 있습니다.
클라이언트(REST Client) 측 구현
REST 클라이언트를 이용해 파일 업로드 구현은 데이터 전송과 크게 차이나지 않습니다.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
procedure TForm1.Button1Click(Sender: TObject);var Filepath: string; Stream: TFileStream;begin if not OpenDialog1.Execute then Exit; Filepath := OpenDialog1.FileName; Stream := TFileStream.Create(Filepath, fmOpenRead); RESTRequest2.Method := rmPOST; RESTRequest2.Resource := 'images/{item}/photo'; RESTRequest2.Params.ParameterByName('item').Value := Edit1.Text; RESTRequest2.Params.AddItem('user_id', '123'); RESTRequest2.Params.AddItem('photo', Stream, pkFILE, [], ctAPPLICATION_OCTET_STREAM); RESTRequest2.Execute; Stream.Free;end; |
파일을 추가할 경우, TStream 이용하므로, TStream을 상속받는 객체들(TFileStream, TMemoryStream 등)을 이용할 수 있습니다.
스트림을 파라미터로 추가하는 코드는 다음과 같습니다.
|
1
|
RESTRequest2.Params.AddItem('photo', Stream, pkFILE, [], ctAPPLICATION_OCTET_STREAM); |
파라미터 종류를 pkFILE로 지정시 내부적으로 multipart/form-data 인코딩 타입으로 데이터가 전송되며, 컨텐트타입을 ctAPPLICATION_OCTET_STREAM으로 지정해야 합니다.
파일 다운로드 구현 방안
서버(RAD Server) 측 구현
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
procedure TImagesResource1.GetItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);var LItem: string; Path: string; Stream: TStream;begin LItem := ARequest.Params.Values['item']; Path := TPath.Combine(ROOT_PATH, 'images'); Path := TPath.Combine(Path, LItem + '.jpg'); if not TFile.Exists(Path) then AResponse.RaiseNotFound('Not found', '''' + LItem + ''' is not found'); Stream := TFileStream.Create(Path, fmOpenRead); AResponse.Body.SetStream(Stream, 'image/jpeg', True);end; |
클라이언트(REST Client) 측 구현
procedure TForm1.Button2Click(Sender: TObject);var WICImage: TWICImage; Stream: TMemoryStream;begin RESTRequest1.Method := rmGET; RESTRequest1.Resource := 'images/{item}/photo'; RESTRequest1.Params.ParameterByName('item').Value := Edit1.Text; RESTRequest1.ExecuteAsync(procedure begin if RESTResponse1.StatusCode = 404 then Exit; Stream := TMemoryStream.Create; Stream.WriteData(RESTResponse1.RawBytes, RESTResponse1.ContentLength); WICImage := TWICImage.Create; WICImage.LoadFromStream(Stream); Image1.Picture.Assign(WICImage); WICImage.Free; Stream.Free; end);end;

RESTUpload.zip