1 module hunt.http.HttpBody;
2 
3 import hunt.http.HttpOutputStream;
4 import hunt.io.HeapByteBuffer;
5 import hunt.io.ByteBuffer;
6 import hunt.io.BufferUtils;
7 
8 import hunt.Exceptions;
9 import hunt.logging;
10 import hunt.util.MimeType;
11 import hunt.util.MimeTypeUtils;
12 
13 import std.array;
14 import std.conv;
15 import std.file;
16 import std.path;
17 import std.range;
18 import std.stdio;
19 import std.traits;
20 
21 /** 
22  * A body content that can be sent or received with an HTTP message.
23  * 
24  * See_Also:
25  *  org.apache.hc.core5.http.HttpEntity
26  */
27 abstract class HttpBody {
28 
29 	string contentType();
30 
31     /**
32      * Returns the number of bytes that will be written to {@code sink} in a call to {@link #writeTo},
33      * or -1 if that count is unknown.
34      */
35 	long contentLength() {
36 		return -1;
37 	}
38 
39     void append(ubyte[] data) {
40         implementationMissing(false);
41     }
42 
43     void append(ByteBuffer buffer) {
44         ubyte[] content = cast(ubyte[])buffer.peekRemaining();
45         append(content);
46     }
47 
48     /** Writes the content of this request to {@code sink}. */
49     void writeTo(HttpOutputStream sink);
50 
51     // getContent()
52 	string asString() {
53         implementationMissing(false);
54         return "";
55     }
56 
57     const(ubyte)[] getRaw() {
58         implementationMissing(false);
59         return null;
60     }
61 
62 	override string toString() {
63 		return asString();
64 	}
65 
66     static HttpBody create(T)(T content) {
67         static if(isSomeString!T) {
68             return create(MimeType.TEXT_HTML_VALUE, content);
69         } else static if(isBasicType!T) {
70             return create(MimeType.TEXT_HTML_VALUE, content.to!string);
71         } else  {
72             // using defaults
73 
74             // TODO: Tasks pending completion -@zhangxueping at 2020-01-08T10:21:19+08:00
75             // return struct and class as json string
76             version(HUNT_HTTP_DEBUG) warningf("Using default conversion for %s", T.stringof);
77             return create(MimeType.TEXT_HTML_VALUE, content.to!string);
78         }
79     }
80     
81     // static HttpBody create(T)(string contentType, T content) if(!isSomeString!T) {
82     //     return create(contentType, content.to!string);
83     // }
84 
85     static HttpBody create(string contentType, long contentLength) {
86         HttpBody impl = new HttpBodyImpl(contentType, contentLength);
87         return impl;
88     }
89 	
90     /**
91      * Returns a new request body that transmits {@code content}. If {@code contentType} is non-null
92      * and lacks a charset, this will use UTF-8.
93      */
94     static HttpBody create(string contentType, string content) {
95         // Charset charset = UTF_8;
96         if (contentType !is null) {
97             // charset = contentType.charset();
98 			string charset = new MimeType(contentType).getCharset();
99             if (charset.empty()) {
100                 // charset = UTF_8;
101                 contentType = contentType ~ ";charset=utf-8";
102             }
103         }
104         ubyte[] bytes = cast(ubyte[])content; // content.getBytes(charset);
105         return create(contentType, bytes);
106     }
107 
108     static HttpBody create(string contentType, ByteBuffer buffer) {
109         ubyte[] content = cast(ubyte[])buffer.peekRemaining();
110         return create(contentType, content);
111     }
112 
113     /** Returns a new request body that transmits {@code content}. */
114     static HttpBody create(string type, const(ubyte)[] content) {
115         // return create(contentType, content, 0, cast(int)content.length);
116         HttpBody impl = new HttpBodyImpl(type, content);
117         return impl;
118     }
119 
120     /** Returns a new request body that transmits the content of {@code file}. */
121     static HttpBody createFromFile(string type, string fileName) {
122         if (fileName.empty()) throw new NullPointerException("fileName is null");
123 		
124 		string rootPath = dirName(thisExePath);
125 		string abslutePath = buildPath(rootPath, fileName);
126 		version(HUNT_HTTP_DEBUG) info("generate request body from file: ", abslutePath);
127 		if(!abslutePath.exists) throw new FileNotFoundException(fileName);
128 
129 		DirEntry entry = DirEntry(abslutePath);
130 		if(entry.isDir()) throw new FileException("Can't handle a direcotry: ", abslutePath);
131 
132 		long total = cast(long)entry.size();
133 		if(total > 10*1024*1024) {
134 			// throw new FileException("The file is too big to upload (< 10MB).");
135 			debug warning("uploading a big file: %d MB", total/1024/1024);
136 		}
137 
138 // dfmt off
139         return new class HttpBody {
140             override string contentType() {
141                 return type;
142             }
143 
144             override long contentLength() {
145                 return total;
146             }
147 
148 
149 	        override string asString() {
150                 // FIXME: Needing refactor or cleanup -@zhangxueping at 2020-01-07T18:58:40+08:00
151                 // 
152                 return fileName;
153             }
154 
155 			override void writeTo(HttpOutputStream sink)  {
156 				version(HUNT_HTTP_DEBUG) infof("loading data from: %s, size: %d", abslutePath, total);
157 				enum MaxBufferSize = 16*1024;
158 				ubyte[] buffer;
159 				
160 				File f = File(abslutePath, "r");
161 				scope(exit) f.close();
162 
163 				size_t remaining = cast(size_t)total;
164 				while(remaining > 0 && !f.eof()) {
165 
166 					if(remaining > MaxBufferSize) 
167 						buffer = new ubyte[MaxBufferSize];
168 					else 
169 						buffer = new ubyte[remaining];
170 					ubyte[] data = f.rawRead(buffer);
171                     
172                     if(data.length > 0) {
173 					    sink.write(cast(byte[])data);
174                         remaining -= cast(int)data.length;
175                     }
176                     version(HUNT_HTTP_DEBUG_MORE) {
177                         tracef("read: %s, remaining: %d, eof: %s", 
178                             data.length, remaining, f.eof());
179                     }
180 				}
181 			}
182         };
183 // dfmt on    
184     }		
185 }
186 
187 
188 class HttpBodyImpl : HttpBody {
189 
190     private Appender!(const(ubyte)[]) _buffer;
191     private string _type;
192     private long _contentLength;
193 
194     this(string type, long contentLength) {
195         _type = type;
196         _contentLength = contentLength;
197     }
198 
199     this(string type, const(ubyte)[] content) {
200         _type = type;
201         _contentLength = cast(long)content.length;
202         append(content);
203     }
204 
205     override string contentType() {
206         return _type;
207     }
208 
209     override long contentLength() {
210         return _contentLength;
211     }
212 
213     override void append(const(ubyte)[] data) {
214         _buffer.put(data);
215     }
216 
217     override string asString() {
218         string str = cast(string)_buffer.data();
219         version(HUNT_DEBUG) {
220             if(_contentLength != -1 && str.length != _contentLength) {
221                 warningf("Mismatched content length, required: %d, actual: %d", _contentLength, str.length);
222             }
223         }
224         return str;
225     }
226 
227     override const(ubyte)[] getRaw() {
228         const(ubyte)[] data = _buffer.data;
229         version(HUNT_DEBUG) {
230             if(_contentLength != -1 && data.length != _contentLength) {
231                 warningf("Mismatched content length, required: %d, actual: %d", _contentLength, data.length);
232             }
233         }
234         return data;
235     }
236 
237     override void writeTo(HttpOutputStream sink) {
238         auto d = cast(byte[])_buffer.data;
239         if(d.length > 0) {
240             sink.write(d, 0, cast(int)d.length);
241         }
242     }
243 }