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 }