├── README.md
├── conf
├── amazon.urls
├── base.urls
└── nginx.conf
├── lib
└── resty
│ └── s3.lua
└── lua-resty-s3-v1.0-1.rockspec
/README.md:
--------------------------------------------------------------------------------
1 | Name
2 | ====
3 |
4 | lua-resty-s3 - upload content to amazon s3 with openresty
5 |
6 | Table of Contents
7 | =================
8 |
9 | * [Name](#name)
10 | * [Status](#status)
11 | * [Description](#description)
12 | * [Synopsis](#synopsis)
13 | * [Methods](#methods)
14 | * [new](#new)
15 | * [generate_auth_headers](#generate_auth_headers)
16 | * [try_upload](#try_upload)
17 | * [upload_url](#upload_url)
18 | * [extract_urls](#extract_urls)
19 | * [upload_content](#upload_content)
20 | * [Limitations](#limitations)
21 | * [Installation](#installation)
22 | * [TODO](#todo)
23 | * [Author](#author)
24 | * [Copyright and License](#copyright-and-license)
25 | * [See Also](#see-also)
26 |
27 | Status
28 | ======
29 |
30 | This library is still under early development and considered experimental.
31 |
32 | Description
33 | ===========
34 |
35 | This Lua library is a s3 uploading utility for the ngx_lua nginx module:
36 |
37 | Synopsis
38 | ========
39 |
40 | ```
41 | lua_package_path "/path/to/lua-resty-s3/lib/?.lua;;";
42 |
43 | server {
44 | location /test {
45 | content_by_lua '
46 | local s3 = require "resty.s3"
47 | local s3, err = s3:new("aws-id", "aws-key")
48 |
49 | final_url, err = s3:upload_url("http://lorempixel.com/400/200/", "examplebucket", "lorempixel400x200")`
50 | ';
51 | }
52 |
53 | include conf/*.urls;
54 | }
55 | ```
56 |
57 | [Back to TOC](#table-of-contents)
58 |
59 | Methods
60 | =======
61 |
62 | All of the commands return either something that evaluates to true on success, or `nil` and an error message on failure.
63 |
64 | new
65 | ---
66 | `syntax: s3, err = s3:new(id, key)`
67 |
68 | Creates an uploading object. In case of failures, returns `nil` and a string describing the error.
69 |
70 | [Back to TOC](#table-of-contents)
71 |
72 | generate_auth_headers
73 | ---------------------
74 | `syntax: s3, err = s3:generate_auth_headers(content_type, destination)`
75 |
76 | `syntax: s3, err = s3:generate_auth_headers("binary/octet-stream", "/examplebucket/lorempixel400x200")`
77 |
78 | Creates the headers needed for authentication with amazon. In case of failures, returns `nil` and a string describing the error.
79 |
80 | [Back to TOC](#table-of-contents)
81 |
82 | try_upload
83 | ----------
84 | `syntax: s3, err = s3:try_upload(content, destination, content_type, headers)`
85 |
86 | `syntax: s3, err = s3:try_upload([[
Hello
]], "/examplebucket/hello", "text/html", headers)`
87 |
88 | Attempts to upload content to s3. In case of failures, returns `nil` and a string describing the error.
89 |
90 | [Back to TOC](#table-of-contents)
91 |
92 | upload_url
93 | ----------
94 | `syntax: final_url, err = s3:upload_url(file_url, bucket, object_name, check_for_existance, add_to_existance)`
95 |
96 | `syntax: final_url, err = s3:upload_url("http://lorempixel.com/400/200/", "/examplebucket/", "lorempixel400x200")`
97 |
98 | Attempts to upload content to s3 from the url set by file_url and the id/key set with new(). If object_name is supplied then that will be the name of the new file, otherwise it will hash the file_url to create a unique key for it.
99 |
100 | Callbacks for checking something before uploading [again], and after uploading can be supplied in check_for_existance and add_to_existance. Each will be called with the object_name or a hash.
101 |
102 | ```
103 | local uploaded_content = ngx.shared.uploaded_content
104 |
105 | check = function (name)
106 | ok, err = uploaded_content:get(name)
107 | if ok then return true end
108 | end
109 |
110 | add = function (name)
111 | ok, err = uploaded_content:add(name)
112 | return true
113 | end
114 |
115 | final_url, err = s3:upload_url("http://lorempixel.com/400/200/", "/examplebucket/", "lorempixel400x200", check, add)
116 | ```
117 |
118 | In case of success, returns the new url. In case of errors, returns `nil` with a string describing the error.
119 |
120 | [Back to TOC](#table-of-contents)
121 |
122 | extract_urls
123 | ------------
124 | `syntax: s3, err = s3:extract_urls(file_content, bucket)`
125 |
126 | `syntax: s3, err = s3:extract_urls([[
]], "/examplebucket/")`
127 |
128 | Attempts to find and upload urls from within source. In case of failures, returns `nil` and a string describing the error.
129 |
130 | [Back to TOC](#table-of-contents)
131 |
132 | upload_content
133 | --------------
134 | `syntax: s3, err = s3:upload_content(file_content, bucket, object_name, check_for_existance, add_to_existance)`
135 |
136 | `syntax: s3, err = s3:upload_content([[
]], "/examplebucket/", "city.jpg")`
137 |
138 | Attempts to upload content to s3 (handles all auth automatically). In case of failures, returns `nil` and a string describing the error.
139 |
140 | [Back to TOC](#table-of-contents)
141 |
142 | Limitations
143 | ===========
144 |
145 |
146 |
147 | [Back to TOC](#table-of-contents)
148 |
149 | Installation
150 | ============
151 | You can install it with luarocks `luarocks install lua-resty-s3`
152 |
153 | Otherwise you need to configure the lua_package_path directive to add the path of your lua-nginx-loggin source to ngx_lua's LUA_PATH search path, as in
154 |
155 | ```nginx
156 | # nginx.conf
157 | http {
158 | lua_package_path "/path/to/lua-resty-s3/lib/?.lua;;";
159 | ...
160 | }
161 | ```
162 |
163 | This package also requires the luasocket and xxhash packages to be installed http://w3.impa.br/~diego/software/luasocket/ , https://github.com/mah0x211/lua-xxhash
164 | ```
165 | luarocks install luasocket
166 | ```
167 |
168 | Ensure that the system account running your Nginx ''worker'' proceses have
169 | enough permission to read the `.lua` file.
170 |
171 | [Back to TOC](#table-of-contents)
172 |
173 | TODO
174 | ====
175 |
176 |
177 |
178 | [Back to TOC](#table-of-contents)
179 |
180 | Author
181 | ======
182 |
183 | James Marlowe "jamesmarlowe" , Lumate LLC.
184 |
185 | [Back to TOC](#table-of-contents)
186 |
187 | Copyright and License
188 | =====================
189 |
190 | This module is licensed under the BSD license.
191 |
192 | Copyright (C) 2012-2014, by James Marlowe (jamesmarlowe) , Lumate LLC.
193 |
194 | All rights reserved.
195 |
196 | Redistribution and use in source and binary forms, with or without
197 | modification, are permitted provided that the following conditions are met:
198 |
199 | * Redistributions of source code must retain the above copyright notice, this
200 | list of conditions and the following disclaimer.
201 |
202 | * Redistributions in binary form must reproduce the above copyright notice,
203 | this list of conditions and the following disclaimer in the documentation
204 | and/or other materials provided with the distribution.
205 |
206 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
207 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
208 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
209 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
210 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
211 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
212 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
213 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
214 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
215 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
216 |
217 | [Back to TOC](#table-of-contents)
218 |
219 | See Also
220 | ========
221 | * the ngx_lua module: http://wiki.nginx.org/HttpLuaModule
222 | * the [lua-resty-hmac](https://github.com/jamesmarlowe/lua-resty-hmac) library
223 |
224 | [Back to TOC](#table-of-contents)
225 |
--------------------------------------------------------------------------------
/conf/amazon.urls:
--------------------------------------------------------------------------------
1 |
2 | location /lua-resty-s3/upload/ {
3 | internal;
4 | resolver 8.8.8.8;
5 | set_unescape_uri $date $arg_date;
6 | set_unescape_uri $auth $arg_auth;
7 | set_unescape_uri $file $arg_file;
8 | set_unescape_uri $mime $arg_mime;
9 |
10 | proxy_pass_request_headers off;
11 | more_clear_headers 'Host';
12 | more_clear_headers 'Connection';
13 | more_clear_headers 'Content-Length';
14 | more_clear_headers 'User-Agent';
15 | more_clear_headers 'Accept';
16 |
17 | proxy_set_header Date $date;
18 | proxy_set_header Authorization $auth;
19 | proxy_set_header content-type $mime;
20 | proxy_set_header Content-MD5 '';
21 |
22 | proxy_pass http://s3.amazonaws.com:80$file;
23 | }
24 |
--------------------------------------------------------------------------------
/conf/base.urls:
--------------------------------------------------------------------------------
1 |
2 | location = /lua-resty-s3/proxy/ {
3 | internal;
4 |
5 | set_unescape_uri $my_host $arg_host;
6 | set_unescape_uri $my_uri $arg_uri;
7 |
8 | resolver 8.8.8.8;
9 |
10 | proxy_set_header User-Agent 'Mozilla/5.0 (X11; Linux x86_64; rv:16.0)';
11 | proxy_pass http://$my_host:80$my_uri;
12 | }
13 |
14 | location = /lua-resty-s3/test/ {
15 | internal;
16 |
17 | content_by_lua '
18 | local s3 = require "s3"
19 |
20 | local params = ngx.req.get_query_args()
21 |
22 | if not params.aws_id or not params.aws_key then
23 | ngx.say("no aws id or key")
24 | ngx.exit(ngx.OK)
25 | end
26 |
27 | local ms3, err = s3:new(params.aws_id, params.aws_key)
28 |
29 | if err then ngx.say(err) ngx.exit(ngx.OK) end
30 |
31 | local final_url, err = ms3:upload_url("http://lorempixel.com/400/200/", params.aws_bucket, "lorempixel400x200")
32 |
33 | if err then
34 | ngx.say(err)
35 | else
36 | ngx.say("File uploaded at: "..final_url)
37 | end
38 |
39 |
40 | local final_url, err = ms3:upload_content([[
]], params.aws_bucket, "city")
41 |
42 | if err then
43 | ngx.say(err)
44 | else
45 | ngx.say("File uploaded at: "..final_url)
46 | end
47 | ';
48 | }
49 |
--------------------------------------------------------------------------------
/conf/nginx.conf:
--------------------------------------------------------------------------------
1 | http {
2 |
3 | lua_package_path "${prefix}/lib/?.lua;;";
4 |
5 | server {
6 |
7 | location /test {
8 | content_by_lua '
9 | local s3 = require "resty.s3"
10 | local s3, err = s3:new("aws-id", "aws-key")
11 |
12 | final_url, err = s3:upload_url("http://lorempixel.com/400/200/", "examplebucket", "lorempixel400x200")`
13 | ';
14 | }
15 |
16 | include *.urls;
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/resty/s3.lua:
--------------------------------------------------------------------------------
1 | -- Copyright (C) James Marlowe (jamesmarlowe), Lumate LLC.
2 |
3 |
4 | local xxhash = require "xxhash"
5 | local url = require "socket.url"
6 | local hmac = require "resty.hmac"
7 | local hash_seed = 0x1db1e298
8 | local upload_url = "/lua-resty-s3/upload/"
9 | local proxy_url = "/lua-resty-s3/proxy/"
10 |
11 |
12 | local ok, new_tab = pcall(require, "table.new")
13 | if not ok then
14 | new_tab = function (narr, nrec) return {} end
15 | end
16 |
17 |
18 | local _M = new_tab(0, 155)
19 | _M._VERSION = '0.01'
20 |
21 |
22 | local mt = { __index = _M }
23 |
24 |
25 | function _M.new(self, id, key)
26 | local id, key = id, key
27 |
28 | if not id then
29 | return nil, "must provide id"
30 | end
31 | if not key then
32 | return nil, "must provide key"
33 | end
34 |
35 | return setmetatable({ id = id, key = key }, mt)
36 | end
37 |
38 |
39 | function _M.generate_auth_headers(self, content_type, destination)
40 | local id, key = self.id, self.key
41 |
42 | if not id or not key then
43 | return nil, "not initialized"
44 | end
45 |
46 | local date = os.date("%a, %d %b %Y %H:%M:%S +0000")
47 | local hm, err = hmac:new(key)
48 | local StringToSign = "PUT"..string.char(10)..string.char(10)..content_type..string.char(10)..date..string.char(10)..destination
49 | headers, err = hm:generate_headers("AWS", id, "sha1", StringToSign)
50 |
51 | return headers, err
52 | end
53 |
54 |
55 | function _M.try_upload(self, content, destination, content_type, headers)
56 | local id, key = self.id, self.key
57 |
58 | if not id or not key then
59 | return nil, "not initialized"
60 | end
61 |
62 | local retry = 0
63 | while (not resp or resp.status ~= 200) and retry < 3 do
64 | resp = ngx.location.capture(
65 | upload_url,
66 | { method = ngx.HTTP_PUT,
67 | body = content,
68 | args = {date=headers.date, auth=headers.auth, file=destination, mime=content_type}}
69 | )
70 | retry = retry + 1
71 | end
72 |
73 | return resp
74 | end
75 |
76 |
77 | function _M.upload_url(self, file_url, bucket, object_name, check_for_existance, add_to_existance)
78 | local id, key = self.id, self.key
79 |
80 | if not id or not key then
81 | return nil, "not initialized"
82 | end
83 |
84 | if not file_url then
85 | return nil, "nothing to upload"
86 | end
87 |
88 | if not bucket then
89 | return nil, "unknown bucket"
90 | end
91 |
92 | if not object_name then object_name = xxhash.xxh32(file_url, hash_seed) end
93 |
94 | local destination = bucket..object_name
95 | local s3_url = "http://s3.amazonaws.com"
96 | local final_url = s3_url..destination
97 | local content_type = "binary/octet-stream"
98 |
99 | if check_for_existance and check_for_existance(object_name) then
100 | return final_url
101 | end
102 |
103 | file_url = file_url:gsub([["]],"")
104 | file_url = url.parse(file_url)
105 | file_content = ngx.location.capture(proxy_url, {args={host=file_url.host,uri=file_url.path}})
106 |
107 | if file_content.status == 200 then
108 | local headers, err = self:generate_auth_headers(content_type, destination)
109 | if not headers then return nil, err end
110 |
111 | local res = self:try_upload(file_content.body, destination, content_type, headers)
112 |
113 | if res.status == 200 then
114 | if add_to_existance then
115 | add_to_existance(object_name)
116 | end
117 |
118 | return final_url
119 | else
120 | return nil, "Could not upload: "..resp.status.." "..resp.body
121 | end
122 | else
123 | return nil, "could not get url: "..url.build(file_url)
124 | end
125 | end
126 |
127 |
128 | function _M.extract_urls(self, file_content, bucket)
129 | pos = 1
130 | for st,sp in function() return file_content:find([["]],pos, true) end do
131 | sp = file_content:find([["]],st+1, true)
132 | embedded_url = file_content:sub(st, sp)
133 |
134 | if embedded_url:find("//", 1, true) then
135 | embedded_url = embedded_url:sub(0,embedded_url:find("?", 1, true))
136 |
137 | if embedded_url:find(".", -6, true) then
138 | object_name = xxhash.xxh32(embedded_url, hash_seed)
139 | new_url, err = self:upload_url(embedded_url, bucket, object_name, check_for_existance, add_to_existance)
140 |
141 | if not new_url then
142 | file_content = file_content:sub(0,st)..file_content:sub(sp)
143 | sp = st+1
144 |
145 | else
146 | file_content = file_content:sub(1,st)..new_url..[["]]..file_content:sub(sp)
147 | sp= st+#new_url+1
148 |
149 | end
150 | else
151 | file_content = file_content:sub(0,st)..file_content:sub(sp)
152 | sp = st+1
153 |
154 | end
155 | end
156 | pos = (sp or (#file_content -1)) + 1
157 | end
158 |
159 | return file_content
160 | end
161 |
162 |
163 | function _M.upload_content(self, file_content, bucket, object_name, check_for_existance, add_to_existance)
164 | local id, key = self.id, self.key
165 |
166 | if not id or not key then
167 | return nil, "not initialized"
168 | end
169 |
170 | if not file_content then
171 | return nil, "nothing to upload"
172 | end
173 |
174 | if not bucket then
175 | return nil, "unknown bucket"
176 | end
177 |
178 | if not object_name then object_name = xxhash.xxh32(file_content, hash_seed) end
179 |
180 | local destination = bucket..object_name
181 | local s3_url = "http://s3.amazonaws.com"
182 | local final_url = s3_url..destination
183 | local content_type = "text/html"
184 |
185 | if check_for_existance and check_for_existance(object_name) then
186 | return final_url
187 | end
188 |
189 | file_content = self:extract_urls(file_content, bucket)
190 |
191 | headers, err = self:generate_auth_headers(content_type, destination)
192 | if not headers then return nil, err end
193 |
194 | local resp = self:try_upload(file_content, destination, content_type, headers)
195 |
196 | if resp.status == 200 then
197 | if add_to_existance then
198 | add_to_existance(object_name)
199 | end
200 | return final_url
201 | else
202 | return nil, "Could not upload: "..resp.status.." "..resp.body
203 | end
204 | end
205 |
206 |
207 |
208 | return _M
209 |
--------------------------------------------------------------------------------
/lua-resty-s3-v1.0-1.rockspec:
--------------------------------------------------------------------------------
1 | package = "lua-resty-s3"
2 | version = "v1.0-1"
3 |
4 | source = {
5 | url = "git://github.com/jamesmarlowe/lua-resty-s3.git"
6 | }
7 |
8 | description = {
9 | summary = "Upload content to amazon s3 with OpenResty",
10 | homepage = "https://github.com/jamesmarlowe/lua-resty-s3",
11 | license = "BSD",
12 | maintainer = "jameskmarlowe@gmail.com"
13 | }
14 |
15 | dependencies = {
16 | "lua >= 5.1",
17 | "lua-resty-hmac",
18 | "luasocket",
19 | "xxhash"
20 | }
21 |
22 | build = {
23 | type = "builtin",
24 | modules = {
25 | ["resty.s3"] = "lib/resty/s3.lua"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------