공통 [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