1 module lighttp.server.resource;
2 
3 import std.array : Appender;
4 import std.base64 : Base64URLNoPadding;
5 import std.digest.crc : crc32Of;
6 import std.string : indexOf, strip;
7 import std.zlib : HeaderFormat, Compress;
8 
9 import lighttp.util : StatusCodes, Http;
10 
11 class Resource {
12 	
13 	private immutable string mime;
14 	
15 	public immutable size_t thresold;
16 	
17 	public const(void)[] uncompressed;
18 	public const(void)[] compressed = null;
19 	
20 	public this(string mime, size_t thresold=512) {
21 		this.mime = mime;
22 		this.thresold = thresold;
23 	}
24 	
25 	public this(string mime, in void[] data) {
26 		this(mime);
27 		this.data = data;
28 	}
29 	
30 	public @property const(void)[] data(in void[] data) {
31 		this.uncompressed = data;
32 		if(data.length >= this.thresold) this.compress();
33 		else this.compressed = null;
34 		return this.uncompressed;
35 	}
36 	
37 	private void compress() {
38 		Compress compress = new Compress(6, HeaderFormat.gzip);
39 		auto data = compress.compress(this.uncompressed);
40 		data ~= compress.flush();
41 		this.compressed = data;
42 	}
43 	
44 	public void apply(Http req, Http res) {
45 		if(this.compressed !is null && req.headers.get("accept-encoding", "").indexOf("gzip") != -1) {
46 			res.headers["Content-Encoding"] = "gzip";
47 			res.body_ = cast(string)this.compressed;
48 		} else {
49 			res.body_ = cast(string)this.uncompressed;
50 		}
51 		res.contentType = this.mime;
52 	}
53 	
54 }
55 
56 class CachedResource : Resource {
57 	
58 	private string etag;
59 	
60 	public this(string mime, size_t thresold=512) {
61 		super(mime, thresold);
62 	}
63 	
64 	public this(string mime, in void[] data) {
65 		super(mime, data);
66 	}
67 	
68 	public override @property const(void)[] data(in void[] data) {
69 		this.etag = Base64URLNoPadding.encode(crc32Of(data));
70 		return super.data(data);
71 	}
72 	
73 	public override void apply(Http req, Http res) {
74 		if(req.headers.get("if-none-match", "") == this.etag) {
75 			res.status = StatusCodes.notModified;
76 		} else {
77 			super.apply(req, res);
78 			res.headers["Cache-Control"] = "public, max-age=31536000";
79 			res.headers["ETag"] = this.etag;
80 		}
81 	}
82 	
83 }
84 
85 class TemplatedResource : Resource {
86 
87 	private abstract class Data {
88 
89 		abstract string translate(string[string]);
90 
91 	}
92 
93 	private class StringData : Data {
94 
95 		string data;
96 
97 		this(string data) {
98 			this.data = data;
99 		}
100 
101 		override string translate(string[string] dictionary) {
102 			return data;
103 		}
104 
105 	}
106 
107 	private class TranslateData : Data {
108 
109 		string key;
110 
111 		this(string key) {
112 			this.key = key;
113 		}
114 
115 		override string translate(string[string] dictionary) {
116 			auto ptr = key in dictionary;
117 			return ptr ? *ptr : "";
118 		}
119 
120 	}
121 	
122 	private Data[] tdata;
123 	
124 	public this(string mime, size_t thresold=512) {
125 		super(mime, thresold);
126 	}
127 	
128 	public this(string mime, in void[] _data) {
129 		this(mime);
130 		string data = cast(string)_data;
131 		while(data.length) {
132 			immutable start = data.indexOf("{{");
133 			if(start != -1) {
134 				this.tdata ~= new StringData(data[0..start]);
135 				data = data[start+2..$];
136 				immutable end = data.indexOf("}}");
137 				if(end != -1) {
138 					this.tdata ~= new TranslateData(data[0..end].strip);
139 					data = data[end+2..$];
140 				}
141 			} else {
142 				this.tdata ~= new StringData(data);
143 				break;
144 			}
145 		}
146 	}
147 	
148 	public Resource apply(string[string] dictionary) {
149 		Appender!string appender;
150 		foreach(data ; this.tdata) {
151 			appender.put(data.translate(dictionary));
152 		}
153 		this.data = appender.data;
154 		return this;
155 	}
156 	
157 }