activestorage-ve-tos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +74 -0
- data/activestorage-ve-tos.gemspec +21 -0
- data/lib/active_storage/service/ve_tos_service.rb +185 -0
- data/lib/activestorage-ve-tos.rb +13 -0
- data/lib/activestorage_ve_tos/version.rb +5 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a368b295543fe0643511c4aac61a97da58d582bf582af34d6fec18d0ff2e9053
|
|
4
|
+
data.tar.gz: 42cc0110680ead98384f52dee47463ea1f8a9c056036527b6157578b80b4d756
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8b35a11c8d6484064e20afd8ef57b48afb569c6a2c0b68ada84341e0ac2fa4711633c5a9c4a14cbe7169db55397f8d76efa8498e914e47ad5e51f06e489e8481
|
|
7
|
+
data.tar.gz: b14474642993b493a73b62e4f2cbf4be01237ac806bc9067390d9e4dba5825ee588ffebddd2425d8bcba831869ab4b1b76c3f8b3a8797f61211c2f60feceaec9
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Renny Ren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# activestorage-ve-tos
|
|
2
|
+
|
|
3
|
+
ActiveStorage adapter for [Volcengine TOS](https://www.volcengine.com/docs/6349). Wraps ve-tos-ruby-sdk.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Gemfile
|
|
9
|
+
gem "activestorage-ve-tos"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then execute:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
bundle install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
# config/storage.yml
|
|
22
|
+
ve_tos:
|
|
23
|
+
service: VeTos
|
|
24
|
+
access_key_id: <%= ENV["TOS_ACCESS_KEY_ID"] %>
|
|
25
|
+
secret_access_key: <%= ENV["TOS_SECRET_ACCESS_KEY"] %>
|
|
26
|
+
region: cn-beijing
|
|
27
|
+
bucket: my-bucket
|
|
28
|
+
# endpoint: tos-cn-beijing.volces.com # default: tos-{region}.volces.com
|
|
29
|
+
# public: false # if true and `host` is set, url returns an unsigned CDN url
|
|
30
|
+
# host: cdn.example.com # custom domain for delivery (replaces TOS host)
|
|
31
|
+
# upload_host: my-bucket.tos-cn-beijing.volces.com # for direct uploads
|
|
32
|
+
# prefix: production # all keys are prefixed with this
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then point your environment at the new service in `config/environments/*.rb`:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
config.active_storage.service = :ve_tos
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Direct uploads
|
|
42
|
+
|
|
43
|
+
`url_for_direct_upload` returns a presigned PUT URL. Configure CORS on your TOS bucket to allow `PUT` from your origin and the headers `Content-Type`, `Content-MD5`, `x-tos-meta-*`.
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
| ActiveStorage method | Backed by |
|
|
48
|
+
| ----------------------- | ------------------------------------------------------------------------------ |
|
|
49
|
+
| `upload` | `PutObject` |
|
|
50
|
+
| `download` (full) | `GetObject` |
|
|
51
|
+
| `download` (with block) | `GetObject` (streaming) |
|
|
52
|
+
| `download_chunk` | `GetObject` with `Range` |
|
|
53
|
+
| `delete` | `DeleteObject` (404 swallowed) |
|
|
54
|
+
| `delete_prefixed` | `ListObjectsV2` + `DeleteMultipleObjects` (paginated) |
|
|
55
|
+
| `exist?` | `HeadObject` |
|
|
56
|
+
| `url` | Presigned GET, or `https://{host}/{key}` when `public: true` and `host` is set |
|
|
57
|
+
| `url_for_direct_upload` | Presigned PUT |
|
|
58
|
+
| `compose` | Streamed concatenation + re-upload |
|
|
59
|
+
|
|
60
|
+
## Caveats
|
|
61
|
+
|
|
62
|
+
- `compose` reads sources fully into memory and re-uploads. Fine for variants/thumbnails, not for very large composites.
|
|
63
|
+
- Multipart upload is not supported by the underlying SDK yet, so `upload` sends the whole body in a single PutObject. Volcengine's per-request limit applies.
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
bundle install
|
|
69
|
+
bundle exec rspec
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/activestorage_ve_tos/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "activestorage-ve-tos"
|
|
7
|
+
spec.version = ActiveStorageVeTos::VERSION
|
|
8
|
+
spec.authors = ["Renny"]
|
|
9
|
+
spec.summary = "ActiveStorage adapter for Volcengine TOS object storage"
|
|
10
|
+
spec.description = "Wraps Volcengine TOS as an ActiveStorage service."
|
|
11
|
+
spec.license = "MIT"
|
|
12
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
13
|
+
|
|
14
|
+
spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE", "activestorage-ve-tos.gemspec"]
|
|
15
|
+
spec.require_paths = ["lib"]
|
|
16
|
+
|
|
17
|
+
spec.add_dependency "ve-tos-ruby-sdk", ">= 0.1.0"
|
|
18
|
+
spec.add_dependency "activestorage", ">= 6.1"
|
|
19
|
+
|
|
20
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
21
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/service"
|
|
4
|
+
require "tos"
|
|
5
|
+
|
|
6
|
+
module ActiveStorage
|
|
7
|
+
class Service::VeTosService < Service
|
|
8
|
+
attr_reader :bucket_name, :prefix, :public, :host, :upload_host
|
|
9
|
+
|
|
10
|
+
def initialize(access_key_id:, secret_access_key:, region:, bucket:,
|
|
11
|
+
endpoint: nil, security_token: nil, public: false,
|
|
12
|
+
host: nil, upload_host: nil, prefix: nil, **)
|
|
13
|
+
@client = TOS::Client.new(
|
|
14
|
+
access_key_id: access_key_id,
|
|
15
|
+
secret_access_key: secret_access_key,
|
|
16
|
+
region: region,
|
|
17
|
+
endpoint: endpoint,
|
|
18
|
+
security_token: security_token,
|
|
19
|
+
)
|
|
20
|
+
@bucket_name = bucket
|
|
21
|
+
@prefix = prefix
|
|
22
|
+
@public = public
|
|
23
|
+
@host = host
|
|
24
|
+
@upload_host = upload_host
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {}, **)
|
|
28
|
+
instrument :upload, key: key, checksum: checksum do
|
|
29
|
+
body = io.respond_to?(:read) ? io.read : io.to_s
|
|
30
|
+
bucket.put_object(
|
|
31
|
+
path_for(key),
|
|
32
|
+
body,
|
|
33
|
+
content_type: content_type,
|
|
34
|
+
content_md5: checksum,
|
|
35
|
+
metadata: custom_metadata,
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def download(key, &block)
|
|
41
|
+
if block_given?
|
|
42
|
+
instrument :streaming_download, key: key do
|
|
43
|
+
bucket.get_object(path_for(key), &block)
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
instrument :download, key: key do
|
|
47
|
+
bucket.get_object(path_for(key)).body
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def download_chunk(key, range)
|
|
53
|
+
instrument :download_chunk, key: key, range: range do
|
|
54
|
+
bucket.get_object(path_for(key), range: range).body
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def delete(key)
|
|
59
|
+
instrument :delete, key: key do
|
|
60
|
+
bucket.delete_object(path_for(key))
|
|
61
|
+
end
|
|
62
|
+
rescue TOS::ServerError => e
|
|
63
|
+
raise unless e.status == 404
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def delete_prefixed(prefix_value)
|
|
67
|
+
instrument :delete_prefixed, prefix: prefix_value do
|
|
68
|
+
loop do
|
|
69
|
+
page = bucket.list_objects(prefix: path_for(prefix_value), max_keys: 1000)
|
|
70
|
+
break if page[:keys].empty?
|
|
71
|
+
|
|
72
|
+
bucket.delete_multiple_objects(page[:keys])
|
|
73
|
+
break unless page[:is_truncated]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def exist?(key)
|
|
79
|
+
instrument :exist, key: key do |payload|
|
|
80
|
+
answer = head?(path_for(key))
|
|
81
|
+
payload[:exist] = answer
|
|
82
|
+
answer
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def url(key, expires_in:, filename:, content_type:, disposition:, **)
|
|
87
|
+
instrument :url, key: key do |payload|
|
|
88
|
+
generated_url = if @public && @host
|
|
89
|
+
public_url(key)
|
|
90
|
+
else
|
|
91
|
+
private_url(key, expires_in: expires_in, filename: filename,
|
|
92
|
+
content_type: content_type, disposition: disposition)
|
|
93
|
+
end
|
|
94
|
+
payload[:url] = generated_url
|
|
95
|
+
generated_url
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
|
100
|
+
instrument :url, key: key do |payload|
|
|
101
|
+
url = @client.presign(
|
|
102
|
+
method: "PUT",
|
|
103
|
+
bucket: @bucket_name,
|
|
104
|
+
key: path_for(key),
|
|
105
|
+
expires_in: expires_in.to_i,
|
|
106
|
+
)
|
|
107
|
+
url = url.sub(/^https:\/\/[^\/]+/, "https://#{@upload_host}") if @upload_host
|
|
108
|
+
payload[:url] = url
|
|
109
|
+
url
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def headers_for_direct_upload(_key, content_type:, checksum:, content_length:, custom_metadata: {}, **)
|
|
114
|
+
headers = {
|
|
115
|
+
"Content-Type" => content_type,
|
|
116
|
+
"Content-Length" => content_length.to_s,
|
|
117
|
+
}
|
|
118
|
+
headers["Content-MD5"] = checksum if checksum
|
|
119
|
+
custom_metadata.each { |k, v| headers["x-tos-meta-#{k}"] = v.to_s }
|
|
120
|
+
headers
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Server-side compose isn't supported by this minimal SDK; download all
|
|
124
|
+
# source keys and re-upload as a single object. Fine for small previews
|
|
125
|
+
# (variants, thumbnails) but not for very large composites.
|
|
126
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
|
127
|
+
buffer = +""
|
|
128
|
+
source_keys.each do |key|
|
|
129
|
+
buffer << bucket.get_object(path_for(key)).body
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
bucket.put_object(
|
|
133
|
+
path_for(destination_key),
|
|
134
|
+
buffer,
|
|
135
|
+
content_type: content_type,
|
|
136
|
+
metadata: custom_metadata,
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def bucket
|
|
143
|
+
@bucket ||= @client.bucket(@bucket_name)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def head?(key)
|
|
147
|
+
bucket.head_object(key)
|
|
148
|
+
true
|
|
149
|
+
rescue TOS::ServerError => e
|
|
150
|
+
return false if e.status == 404
|
|
151
|
+
|
|
152
|
+
raise
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def path_for(key)
|
|
156
|
+
[@prefix, key].compact.reject(&:empty?).join("/").gsub(%r{^/+}, "").squeeze("/")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def public_url(key)
|
|
160
|
+
"https://#{@host}/#{@client.escape_key(path_for(key))}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def private_url(key, expires_in:, filename: nil, content_type: nil, disposition: nil)
|
|
164
|
+
query = {}
|
|
165
|
+
if filename
|
|
166
|
+
wrapped = ActiveStorage::Filename.wrap(filename)
|
|
167
|
+
query["response-content-disposition"] = content_disposition_with(type: disposition, filename: wrapped)
|
|
168
|
+
end
|
|
169
|
+
query["response-content-type"] = content_type if content_type
|
|
170
|
+
|
|
171
|
+
url = @client.presign(
|
|
172
|
+
method: "GET",
|
|
173
|
+
bucket: @bucket_name,
|
|
174
|
+
key: path_for(key),
|
|
175
|
+
query: query,
|
|
176
|
+
expires_in: expires_in.to_i,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if @host
|
|
180
|
+
url = url.sub(/^https:\/\/[^\/]+/, "https://#{@host}")
|
|
181
|
+
end
|
|
182
|
+
url
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "tos"
|
|
5
|
+
|
|
6
|
+
require "activestorage_ve_tos/version"
|
|
7
|
+
require "active_storage/service/ve_tos_service"
|
|
8
|
+
|
|
9
|
+
# When `service: VeTos` is configured in `config/storage.yml`, ActiveStorage
|
|
10
|
+
# resolves it to `ActiveStorage::Service::VeTosService` via Ruby constant
|
|
11
|
+
# lookup — no extra wiring is needed.
|
|
12
|
+
module ActiveStorageVeTos
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activestorage-ve-tos
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Renny
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ve-tos-ruby-sdk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activestorage
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.12'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.12'
|
|
54
|
+
description: Wraps Volcengine TOS as an ActiveStorage service.
|
|
55
|
+
executables: []
|
|
56
|
+
extensions: []
|
|
57
|
+
extra_rdoc_files: []
|
|
58
|
+
files:
|
|
59
|
+
- LICENSE
|
|
60
|
+
- README.md
|
|
61
|
+
- activestorage-ve-tos.gemspec
|
|
62
|
+
- lib/active_storage/service/ve_tos_service.rb
|
|
63
|
+
- lib/activestorage-ve-tos.rb
|
|
64
|
+
- lib/activestorage_ve_tos/version.rb
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata: {}
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 2.7.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 4.0.11
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: ActiveStorage adapter for Volcengine TOS object storage
|
|
85
|
+
test_files: []
|