oss-ar 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 79aec43def861f705ca99fba23e6272e42beb2285c26c0eb8e49fe1b000ef954
4
+ data.tar.gz: 4a9d2644fb91513fc45fe10c93d8d6ce3d66afac4c36c12f6b4b8af36681f4d2
5
+ SHA512:
6
+ metadata.gz: b870eb437f04415281f9258511234da6d5b71b2fc1de5a414c678ee5e1752ee7aab98facd3beb59db7edf6830a0a83f0e2d66822e94263bc8aad07c55436eaf4
7
+ data.tar.gz: 62e94d621972d750cba9f201415911dce13d3e83baaf24ec71bc8a649e0b52d6e9d3669b14c12b17cdbf136cc7b3ab85effbaa32604ab0e8b5e59280298e6414
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in oss-activerecord.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem "rake", "~> 13.0"
10
+ end
11
+
12
+ group :test do
13
+ gem "rspec", "~> 3.0"
14
+ end
15
+
16
+ gem "activerecord", "~> 7.1"
17
+ gem "sqlite3", "~> 1.4"
18
+ gem "sorbet-runtime", "~> 0.5.11357"
19
+
20
+ gem "aliyun-sdk", "~> 0.8.0"
21
+ gem "qiniu", "~> 6.9"
data/Gemfile.lock ADDED
@@ -0,0 +1,93 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ oss-ar (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activemodel (7.1.3.2)
10
+ activesupport (= 7.1.3.2)
11
+ activerecord (7.1.3.2)
12
+ activemodel (= 7.1.3.2)
13
+ activesupport (= 7.1.3.2)
14
+ timeout (>= 0.4.0)
15
+ activesupport (7.1.3.2)
16
+ base64
17
+ bigdecimal
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ connection_pool (>= 2.2.5)
20
+ drb
21
+ i18n (>= 1.6, < 2)
22
+ minitest (>= 5.1)
23
+ mutex_m
24
+ tzinfo (~> 2.0)
25
+ aliyun-sdk (0.8.0)
26
+ nokogiri (~> 1.6)
27
+ rest-client (~> 2.0)
28
+ base64 (0.2.0)
29
+ bigdecimal (3.1.7)
30
+ concurrent-ruby (1.2.3)
31
+ connection_pool (2.4.1)
32
+ diff-lcs (1.5.1)
33
+ domain_name (0.6.20240107)
34
+ drb (2.2.1)
35
+ http-accept (1.7.0)
36
+ http-cookie (1.0.5)
37
+ domain_name (~> 0.5)
38
+ i18n (1.14.4)
39
+ concurrent-ruby (~> 1.0)
40
+ mime-types (3.5.2)
41
+ mime-types-data (~> 3.2015)
42
+ mime-types-data (3.2024.0305)
43
+ minitest (5.22.3)
44
+ mutex_m (0.2.0)
45
+ netrc (0.11.0)
46
+ nokogiri (1.16.4-x86_64-darwin)
47
+ racc (~> 1.4)
48
+ qiniu (6.9.1)
49
+ mime-types (~> 3.1)
50
+ rest-client (~> 2.0)
51
+ rexml (~> 3.2)
52
+ racc (1.7.3)
53
+ rake (13.2.1)
54
+ rest-client (2.1.0)
55
+ http-accept (>= 1.7.0, < 2.0)
56
+ http-cookie (>= 1.0.2, < 2.0)
57
+ mime-types (>= 1.16, < 4.0)
58
+ netrc (~> 0.8)
59
+ rexml (3.2.6)
60
+ rspec (3.13.0)
61
+ rspec-core (~> 3.13.0)
62
+ rspec-expectations (~> 3.13.0)
63
+ rspec-mocks (~> 3.13.0)
64
+ rspec-core (3.13.0)
65
+ rspec-support (~> 3.13.0)
66
+ rspec-expectations (3.13.0)
67
+ diff-lcs (>= 1.2.0, < 2.0)
68
+ rspec-support (~> 3.13.0)
69
+ rspec-mocks (3.13.0)
70
+ diff-lcs (>= 1.2.0, < 2.0)
71
+ rspec-support (~> 3.13.0)
72
+ rspec-support (3.13.1)
73
+ sorbet-runtime (0.5.11357)
74
+ sqlite3 (1.7.3-x86_64-darwin)
75
+ timeout (0.4.1)
76
+ tzinfo (2.0.6)
77
+ concurrent-ruby (~> 1.0)
78
+
79
+ PLATFORMS
80
+ x86_64-darwin-22
81
+
82
+ DEPENDENCIES
83
+ activerecord (~> 7.1)
84
+ aliyun-sdk (~> 0.8.0)
85
+ oss-ar!
86
+ qiniu (~> 6.9)
87
+ rake (~> 13.0)
88
+ rspec (~> 3.0)
89
+ sorbet-runtime (~> 0.5.11357)
90
+ sqlite3 (~> 1.4)
91
+
92
+ BUNDLED WITH
93
+ 2.4.6
data/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # oss-activerecord
2
+
3
+ 一种用于将国内的云存储服务与 ActiveRecord 集成的解决方案。
4
+
5
+ ## 写在最前
6
+
7
+ 该 gem 主要包括两个部分:
8
+
9
+ 1. 一个 OSS 服务的适配器,将不同的 OSS 服务抽象为统一的接口,方便在不同的 OSS 服务商之间切换。
10
+ 2. 一个 OSS::Attachment 数据表,将 OSS 上传的对象纳入在一个表中管理,方便日后的迁移。
11
+
12
+ ## 安装
13
+
14
+ 在 Gemfile 中添加:
15
+
16
+ ```ruby
17
+ gem 'oss-activerecord'
18
+ ```
19
+
20
+ 然后执行:
21
+
22
+ ```bash
23
+ $ bundle
24
+ ```
25
+
26
+ 最后在 Ruby 项目中引入:
27
+
28
+ ```ruby
29
+ require 'oss-activerecord'
30
+ ```
31
+
32
+ ## OSS Adapter
33
+
34
+ 没有为 OSS Adapter 提供一个统一的类,而是提供不同的云厂商的适配器,你可以根据自己的需求选择合适的适配器。(这么做是因为 Ruby 语言不需要)
35
+
36
+ 目前已支持的 OSS 服务商有:
37
+
38
+ - 阿里云 OSS:`OSS::Adapters::AliyunAdapter`
39
+ - 七牛云 OSS:`OSS::Adapters::QiniuAdapter`
40
+
41
+ ### 创建 adapter
42
+
43
+ 创建遵从相同的接口签名,以阿里云 OSS 为例:
44
+
45
+ ```ruby
46
+ gem "aliyun-sdk"
47
+
48
+ require 'oss/adapters/aliyun_adapter'
49
+ adapter = OSS::Adapters::AliyunAdapter.new(
50
+ access_key: 'your-access-key-id',
51
+ secret_key: 'your-access-key-secret',
52
+ endpoint: 'your-endpoint',
53
+ bucket: 'your-bucket'
54
+ )
55
+ ```
56
+
57
+ 以七牛云 OSS 为例:
58
+
59
+ ```ruby
60
+ gem "qiniu"
61
+
62
+ require 'oss/adapters/qiniu_adapter'
63
+ adapter = OSS::Adapters::QiniuAdapter.new(
64
+ access_key: 'your-access-key',
65
+ secret_key: 'your-secret-key',
66
+ endpoint: 'your-endpoint',
67
+ bucket: 'your-bucket'
68
+ )
69
+ ```
70
+
71
+ ### 上传文件
72
+
73
+ ```ruby
74
+ adapter.upload('path/to/file', 'object-key')
75
+ ```
76
+
77
+ ### 返回签名 URL
78
+
79
+ ```ruby
80
+ adapter.signed_url('object-key')
81
+ ```
82
+
83
+ ## `OSS::Attachment`
84
+
85
+ 该模块主要是将所有与云存储对象相关的属性都关联一个表,方便统一管理和日后的迁移。同时,为了维护项目本身的兼容性,也为了适应更常见的需求,该模块将表关联的操作透明化。
86
+
87
+ ### 创建表
88
+
89
+ 在使用该模块之前,要求系统中已经存在一个 `oss_attachments` 表,类似的迁移脚本:
90
+
91
+ ```ruby
92
+ create_table :attachments do |t|
93
+ t.string :object_key, comment: 'OSS 对象的 key'
94
+ t.references :attachable, polymorphic: true, comment: '关联的对象'
95
+ t.string :context, comment: '属性上下文'
96
+ end
97
+ ```
98
+
99
+ ### 关联模型
100
+
101
+ 以 `User` 模型为例:
102
+
103
+ ```ruby
104
+ class User < ActiveRecord::Base
105
+ extend OSS::Attachment::Attachable
106
+
107
+ has_one_attached :avatar, :avatar_url
108
+ end
109
+ ```
110
+
111
+ 该动作创建了一个 `avatar` 关联,同时创建了一个 `avatar_url` 的属性用于兼容性操作。
112
+
113
+ ### 使用属性 URL
114
+
115
+ ```ruby
116
+ user = User.new
117
+
118
+ # 保存 URL
119
+ user.avatar_url = 'object_key' # or full url、full url with signature
120
+ user.save! # 这一步才会写入数据库
121
+
122
+ # 读取 URL
123
+ user.avatar_url # get full url
124
+ ```
125
+
126
+ ### has_many_attached
127
+
128
+ 除了 `has_one_attached` 之外,还支持 `has_many_attached`:
129
+
130
+ ```ruby
131
+ class User < ActiveRecord::Base
132
+ extend OSS::Attachment::Attachable
133
+
134
+ has_many_attached :photos, :photo_urls
135
+ end
136
+ ```
137
+
138
+ 此时可以使用 `photo_urls` 进行操作。
139
+
140
+ ```ruby
141
+ user = User.new
142
+
143
+ # 保存 URL
144
+ user.photo_urls = ['object_key1', 'object_key2'] # or full urls、full urls with signature
145
+ user.save! # 这一步才会写入数据库
146
+
147
+ # 读取 URL
148
+ user.photo_urls # get full urls
149
+ ```
150
+
151
+ ## 开发
152
+
153
+ ```bash
154
+ git clone 本仓库
155
+
156
+ # 安装依赖项目
157
+ bundle
158
+
159
+ # 运行单元测试
160
+ bundle exec rspec
161
+
162
+ # 生成 gem
163
+ gem build oss-activerecord.gemspec
164
+
165
+ # 推送 gem
166
+ gem push oss-activerecord-x.x.x.gem
167
+ ```
168
+
169
+ ## 贡献
170
+
171
+ 问题报告和 pull requests 在 GitHub 上:https://github.com/[USERNAME]/meta-oss.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aliyun/oss'
4
+
5
+ module OSS
6
+ module Adapters
7
+ class AliyunAdapter
8
+ extend T::Sig
9
+
10
+ sig {params(endpoint: String, access_key: String, secret_key: String, bucket: String).void}
11
+ def initialize(endpoint:, access_key:, secret_key:, bucket:)
12
+ @client = Aliyun::OSS::Client.new(
13
+ endpoint: endpoint,
14
+ access_key_id: access_key,
15
+ access_key_secret: secret_key
16
+ )
17
+ @bucket = @client.get_bucket(bucket)
18
+ end
19
+
20
+ sig {params(filepath: String, object_key: String).void}
21
+ def upload(filepath, object_key)
22
+ @bucket.put_object(object_key, file: filepath)
23
+ end
24
+
25
+ sig {params(object_key: String).returns(String)}
26
+ def signed_url(object_key)
27
+ @bucket.object_url(object_key, true, 60)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qiniu'
4
+
5
+ module OSS
6
+ module Adapters
7
+ class QiniuAdapter
8
+ extend T::Sig
9
+
10
+ sig {params(endpoint: String, access_key: String, secret_key: String, bucket: String).void}
11
+ def initialize(endpoint: ,access_key:, secret_key:, bucket:)
12
+ Qiniu.establish_connection! access_key: access_key, secret_key: secret_key
13
+ @endpoint = endpoint
14
+ @bucket = bucket
15
+ end
16
+
17
+ sig {params(filepath: String, object_key: String).void}
18
+ def upload(filepath, object_key)
19
+ # 构建上传策略,上传策略的更多参数请参照 http://developer.qiniu.com/article/developer/security/put-policy.html
20
+ put_policy = Qiniu::Auth::PutPolicy.new(
21
+ @bucket, # 存储空间
22
+ object_key, # 指定上传的资源名,如果传入 nil,就表示不指定资源名,将使用默认的资源名
23
+ 3600 # token 过期时间,默认为 3600 秒,即 1 小时
24
+ )
25
+ # 生成上传 Token
26
+ uptoken = Qiniu::Auth.generate_uptoken(put_policy)
27
+ # 调用 upload_with_token_2 方法上传
28
+ Qiniu::Storage.upload_with_token_2(
29
+ uptoken,
30
+ filepath,
31
+ object_key,
32
+ nil, # 可以接受一个 Hash 作为自定义变量,请参照 http://developer.qiniu.com/article/kodo/kodo-developer/up/vars.html#xvar
33
+ bucket: @bucket
34
+ )
35
+ end
36
+
37
+ sig {params(object_key: String).returns(String)}
38
+ def signed_url(object_key)
39
+ primitive_url = File.join(@endpoint, object_key)
40
+ Qiniu::Auth.authorize_download_url(primitive_url)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module OSS
6
+ class Attachment < ActiveRecord::Base
7
+ extend T::Sig
8
+
9
+ self.primary_key = :object_key
10
+
11
+ validates :object_key, presence: true, uniqueness: { scope: %i[attachable_id attachable_type context] }
12
+
13
+ belongs_to :attachable, polymorphic: true, optional: false, autosave: true
14
+
15
+ sig {returns(T.nilable(String))}
16
+ def url
17
+ return object_key if object_key.nil? || full_url?(object_key)
18
+
19
+ if self.class.use_signed_url
20
+ self.class.oss_adapter.signed_url(object_key)
21
+ else
22
+ File.join(self.class.endpoint, object_key)
23
+ end
24
+ end
25
+
26
+ sig {params(url: T.nilable(String)).void}
27
+ def url=(url)
28
+ if url.nil?
29
+ self.object_key = nil
30
+ else
31
+ self.object_key = self.class.resolve_object_key(url)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def full_url?(url)
38
+ url.start_with?(%r{https?://})
39
+ end
40
+
41
+ class << self
42
+ extend T::Sig
43
+
44
+ attr_accessor :endpoint, :use_signed_url, :oss_adapter
45
+
46
+ sig {params(object_key: String, filepath: String).returns(String)}
47
+ def upload(object_key, filepath)
48
+ oss_adapter.upload(object_key, filepath)
49
+ end
50
+
51
+ sig {params(object_key: String).returns(String)}
52
+ def signed_url(object_key)
53
+ oss_adapter.signed_url(object_key)
54
+ end
55
+
56
+ sig {params(url: String).returns(String)}
57
+ def resolve_object_key(url)
58
+ return url unless descendant_of_endpoint?(url)
59
+
60
+ # 首先去掉 url 的 query 部分
61
+ url = url.split('?').first
62
+ # 然后获取相对路径
63
+ p = Pathname.new(url).relative_path_from(Pathname.new(endpoint))
64
+
65
+ p.to_s
66
+ end
67
+
68
+ sig {params(object_key: String).returns(String)}
69
+ def resolve_full_url(object_key)
70
+ return object_key if object_key.nil? || full_url?(object_key)
71
+
72
+ if use_signed_url
73
+ oss_adapter.signed_url(object_key)
74
+ else
75
+ File.join(endpoint, object_key)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def full_url?(url)
82
+ url.start_with?(%r{https?://})
83
+ end
84
+
85
+ def descendant_of_endpoint?(url)
86
+ endpoint = self.endpoint
87
+
88
+ # 确保尾部有斜杠
89
+ endpoint = "#{endpoint}/" unless endpoint.end_with?('/')
90
+ url = "#{url}/" unless url.end_with?('/')
91
+
92
+ # 检查 url 是否是 endpoint 的子路径
93
+ url.start_with?(endpoint)
94
+ end
95
+ end
96
+
97
+ module Attachable
98
+ extend T::Sig
99
+
100
+ sig {params(association_name: Symbol, attribute_name: Symbol).void}
101
+ def has_one_attached(association_name, attribute_name)
102
+ has_one association_name, ->{ where(context: association_name) }, class_name: 'OSS::Attachment', as: :attachable
103
+
104
+ define_method("#{attribute_name}=") do |url|
105
+ attachment = send(association_name) || Attachment.new(context: association_name)
106
+ object_key = Attachment.resolve_object_key(url)
107
+ attachment.object_key = object_key
108
+ send("#{association_name}=", attachment)
109
+ end
110
+
111
+ define_method(attribute_name) do
112
+ record = send(association_name)
113
+ record&.url
114
+ end
115
+ end
116
+
117
+ sig {params(association_name: Symbol, attribute_name: Symbol).void}
118
+ def has_many_attached(association_name, attribute_name)
119
+ has_many association_name, ->{ where context: association_name }, class_name: 'OSS::Attachment', as: :attachable, dependent: :delete_all, validate: false
120
+
121
+ define_method("#{attribute_name}=") do |urls|
122
+ object_keys = urls.map { |url| Attachment.resolve_object_key(url) }
123
+
124
+ attachments = object_keys.map do |object_key|
125
+ Attachment.new(context: association_name, object_key: object_key)
126
+ end
127
+ send("#{association_name}=", attachments)
128
+ end
129
+
130
+ define_method(attribute_name) do
131
+ record = send(association_name)
132
+ record.map(&:url)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OSS
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require_relative "oss-activerecord/attachment"
5
+
6
+ module Meta
7
+ module OSS
8
+ class Error < StandardError; end
9
+ # Your code goes here...
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oss-ar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - yetrun
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-05-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - yetrun@foxmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - Gemfile
22
+ - Gemfile.lock
23
+ - README.md
24
+ - Rakefile
25
+ - lib/oss-activerecord.rb
26
+ - lib/oss-activerecord/attachment.rb
27
+ - lib/oss-activerecord/version.rb
28
+ - lib/oss/adapters/aliyun_adapter.rb
29
+ - lib/oss/adapters/qiniu_adapter.rb
30
+ homepage: https://github.com/yetrun/oss-activerecord
31
+ licenses:
32
+ - LGPL-2.0
33
+ metadata:
34
+ homepage_uri: https://github.com/yetrun/oss-activerecord
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.4.6
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: 将国内的云存储服务与 ActiveRecord 集成的解决方案
54
+ test_files: []