1 /**
2 	D library API for Amazon S3
3 
4 	Copyright: © 2015 sigod
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE file.
6 	Authors: sigod
7 */
8 module s3.s3;
9 
10 private {
11 	import s3.internal.helpers;
12 
13 	import std.datetime : Clock, UTC;
14 	import std.exception : enforce;
15 	import std.net.curl;
16 	import std.range;
17 }
18 
19 class S3Client
20 {
21 	private string _access_key;
22 	private string _secret_key;
23 
24 	this(string access_key, string secret_key)
25 	{
26 		_access_key = access_key;
27 		_secret_key = secret_key;
28 	}
29 
30 	private string endpoint = "s3.amazonaws.com";
31 
32 	void putObject(R)(PutObjectRequest!R request)
33 	{
34 		auto client = HTTP(request.bucket ~ "." ~ endpoint ~ request.key);
35 
36 		client.method = HTTP.Method.put;
37 
38 		auto date = Clock.currTime(UTC()).toRFC822DateTime();
39 
40 		static if (!__traits(compiles, { HTTP client; client.contentLength = ulong.max; })) {
41 			assert(request.content_size <= uint.max, "uploading files bigger than uint.max isn't supported "
42 								~ "under x86 platform in this version of Phobos");
43 
44 			client.contentLength = cast(uint)request.content_size;
45 		}
46 		else
47 			client.contentLength = request.content_size;
48 
49 		client.addRequestHeader("Date", date);
50 		client.addRequestHeader("x-amz-acl", "public-read");
51 		client.addRequestHeader("Content-Type", "image/jpeg");
52 		client.addRequestHeader("Authorization",
53 			_authHeader("PUT", "", "image/jpeg", date, _cannedResource(request.bucket, request.key), "x-amz-acl:public-read")
54 		);
55 
56 		void[] m = void;
57 
58 		auto content = request.content;
59 
60 		if (!content.empty)
61 			m = content.front;
62 
63 		client.onSend = delegate size_t(void[] data)
64 		{
65 			if (content.empty) return 0;
66 			else if (m.length == 0) {
67 				content.popFront();
68 
69 				if (content.empty) return 0;
70 
71 				m = content.front;
72 			}
73 
74 			size_t length = m.length > data.length ? data.length : m.length;
75 			if (length == 0) return 0;
76 
77 			data[0 .. length] = m[0 .. length];
78 			m = m[length .. $];
79 
80 			return length;
81 		};
82 
83 		client.perform();
84 
85 		enforce(client.statusLine.code == 200);
86 	}
87 
88 	void deleteObject(string bucket, string key)
89 	{
90 		auto client = HTTP(bucket ~ "." ~ endpoint ~ key);
91 
92 		client.method = HTTP.Method.del;
93 
94 		auto date = Clock.currTime(UTC()).toRFC822DateTime();
95 
96 		client.addRequestHeader("Date", date);
97 		client.addRequestHeader("Authorization", _authHeader("DELETE", "", "", date, _cannedResource(bucket, key)));
98 
99 		client.perform();
100 
101 		enforce(client.statusLine.code == 204);
102 	}
103 
104 	private string _authHeader(string method, string md5, string type, string date, string cannedResource, string x_amz = "")
105 	{
106 		import std.base64;
107 		import std.format : format;
108 		import std.utf : toUTF8;
109 		import std.string : representation;
110 
111 		string part = format("%s\n%s\n%s\n%s\n", method, md5, type, date);
112 		string signature = Base64.encode(hmac_sha1(
113 			_secret_key.representation,
114 			toUTF8(part ~ (x_amz == "" ? "" : x_amz ~ "\n") ~ cannedResource).representation
115 		));
116 
117 		return format("AWS %s:%s", _access_key, signature);
118 	}
119 
120 	private string _cannedResource(string bucket, string key)
121 	{
122 		import std.algorithm : startsWith;
123 
124 		assert(key.startsWith('/'), "keys must always start with `/`");
125 
126 		return "/" ~ bucket ~ key;
127 	}
128 }
129 
130 struct PutObjectRequest(Range)
131 	if (isInputRange!Range && is(ElementType!Range == ubyte[]))
132 {
133 	string bucket;
134 	string key;
135 	Range content;
136 	ulong content_size;
137 }
138 
139 auto putObjectRequest(string bucket, string key, string file)
140 {
141 	import std.stdio : File;
142 
143 	enum chunk_size = 16 * 1024; // 16 KiB
144 	auto file_ = File(file, "r");
145 
146 	return PutObjectRequest!(File.ByChunk)(bucket, key, file_.byChunk(chunk_size), file_.size);
147 }