├── test ├── fixtures │ ├── foo.gif │ ├── foo.jpg │ ├── foo.zip │ └── 中文 文件测试.zip ├── aliyun_file_test.rb ├── aliyun_test.rb ├── test_helper.rb └── bucket_test.rb ├── .rubocop.yml ├── lib ├── carrierwave │ ├── aliyun │ │ ├── version.rb │ │ ├── configuration.rb │ │ └── bucket.rb │ └── storage │ │ ├── aliyun.rb │ │ └── aliyun_file.rb └── carrierwave-aliyun.rb ├── .gitignore ├── Gemfile ├── Rakefile ├── .github └── workflows │ └── build.yml ├── carrierwave-aliyun.gemspec ├── Gemfile.lock ├── CHANGELOG.md └── README.md /test/fixtures/foo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huacnlee/carrierwave-aliyun/HEAD/test/fixtures/foo.gif -------------------------------------------------------------------------------- /test/fixtures/foo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huacnlee/carrierwave-aliyun/HEAD/test/fixtures/foo.jpg -------------------------------------------------------------------------------- /test/fixtures/foo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huacnlee/carrierwave-aliyun/HEAD/test/fixtures/foo.zip -------------------------------------------------------------------------------- /test/fixtures/中文 文件测试.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huacnlee/carrierwave-aliyun/HEAD/test/fixtures/中文 文件测试.zip -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | Style/StringLiterals: 4 | Enabled: true 5 | EnforcedStyle: double_quotes 6 | -------------------------------------------------------------------------------- /lib/carrierwave/aliyun/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CarrierWave 4 | module Aliyun 5 | VERSION = "1.2.3" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | coverage/ 9 | uploads/ 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord" 6 | gem "mini_magick" 7 | gem "minitest" 8 | gem "rack-test" 9 | gem "rake" 10 | gem "sqlite3" 11 | gemspec 12 | -------------------------------------------------------------------------------- /lib/carrierwave-aliyun.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "carrierwave" 4 | require "carrierwave/storage/aliyun" 5 | require "carrierwave/storage/aliyun_file" 6 | require "carrierwave/aliyun/bucket" 7 | require "carrierwave/aliyun/version" 8 | require "carrierwave/aliyun/configuration" 9 | require "aliyun/oss" 10 | 11 | CarrierWave::Uploader::Base.send(:include, CarrierWave::Aliyun::Configuration) 12 | 13 | if CarrierWave::VERSION <= "0.11.0" 14 | require "carrierwave/processing/mime_types" 15 | CarrierWave::Uploader::Base.send(:include, CarrierWave::MimeTypes) 16 | CarrierWave::Uploader::Base.send(:process, :set_content_type) 17 | end 18 | 19 | Aliyun::Common::Logging.set_log_file("/dev/null") 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "bundler/setup" 5 | rescue LoadError 6 | puts "You must `gem install bundler` and `bundle install` to run rake tasks" 7 | end 8 | 9 | require "rdoc/task" 10 | 11 | RDoc::Task.new(:rdoc) do |rdoc| 12 | rdoc.rdoc_dir = "rdoc" 13 | rdoc.title = "Carrierwave Aliyun" 14 | rdoc.options << "--line-numbers" 15 | rdoc.rdoc_files.include("README.md") 16 | rdoc.rdoc_files.include("lib/**/*.rb") 17 | end 18 | 19 | require "bundler/gem_tasks" 20 | 21 | require "rake/testtask" 22 | 23 | Rake::TestTask.new(:test) do |t| 24 | t.libs << "test" 25 | t.pattern = "test/**/*_test.rb" 26 | t.verbose = false 27 | t.warning = false 28 | end 29 | 30 | task default: :test 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: push 3 | jobs: 4 | build: 5 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | include: 11 | - ruby: 2.6 12 | gemfile: Gemfile 13 | env: 14 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 15 | USE_OFFICIAL_GEM_SOURCE: 1 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby }} 21 | bundler-cache: true 22 | - name: "Test" 23 | env: 24 | ALIYUN_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }} 25 | ALIYUN_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} 26 | run: bundle exec rake test -------------------------------------------------------------------------------- /lib/carrierwave/aliyun/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CarrierWave 4 | module Aliyun 5 | module Configuration 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | # Deprecation 10 | add_config :aliyun_access_id 11 | add_config :aliyun_access_key 12 | add_config :aliyun_area 13 | add_config :aliyun_private_read 14 | 15 | add_config :aliyun_access_key_id 16 | add_config :aliyun_access_key_secret 17 | add_config :aliyun_bucket 18 | 19 | add_config :aliyun_region 20 | add_config :aliyun_internal 21 | add_config :aliyun_host 22 | add_config :aliyun_mode 23 | 24 | configure do |config| 25 | config.storage_engines[:aliyun] = "CarrierWave::Storage::Aliyun" 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /carrierwave-aliyun.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # frozen_string_literal: true 3 | 4 | 5 | $LOAD_PATH.push File.expand_path("../lib", __FILE__) 6 | require File.expand_path("lib/carrierwave/aliyun/version") 7 | 8 | Gem::Specification.new do |s| 9 | s.name = "carrierwave-aliyun" 10 | s.version = CarrierWave::Aliyun::VERSION 11 | s.platform = Gem::Platform::RUBY 12 | s.authors = ["Jason Lee"] 13 | s.email = ["huacnlee@gmail.com"] 14 | s.homepage = "https://github.com/huacnlee/carrierwave-aliyun" 15 | s.summary = "Aliyun OSS support for Carrierwave" 16 | s.description = "Aliyun OSS support for Carrierwave" 17 | s.files = Dir.glob("lib/**/*.rb") + %w(README.md CHANGELOG.md) 18 | s.require_paths = ["lib"] 19 | s.license = "MIT" 20 | 21 | s.add_dependency "carrierwave", [">= 1"] 22 | s.add_dependency "aliyun-sdk", [">= 0.7.0"] 23 | end 24 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/aliyun.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CarrierWave 4 | module Storage 5 | class Aliyun < Abstract 6 | def store!(new_file) 7 | f = AliyunFile.new(uploader, self, uploader.store_path) 8 | headers = { 9 | content_type: new_file.content_type, 10 | content_disposition: uploader.try(:content_disposition) 11 | } 12 | 13 | f.store(new_file, headers) 14 | f 15 | end 16 | 17 | def retrieve!(identifier) 18 | AliyunFile.new(uploader, self, uploader.store_path(identifier)) 19 | end 20 | 21 | def cache!(new_file) 22 | f = AliyunFile.new(uploader, self, uploader.cache_path) 23 | headers = { 24 | content_type: new_file.content_type, 25 | content_disposition: uploader.try(:content_disposition) 26 | } 27 | 28 | f.store(new_file, headers) 29 | f 30 | end 31 | 32 | def retrieve_from_cache!(identifier) 33 | AliyunFile.new(uploader, self, uploader.cache_path(identifier)) 34 | end 35 | 36 | def delete_dir!(path) 37 | # do nothing, because there's no such things as 'empty directory' 38 | end 39 | 40 | private 41 | 42 | def bucket 43 | @bucket ||= CarrierWave::Aliyun::Bucket.new(uploader) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/aliyun_file_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class CarrierWave::Storage::AliyunFileTest < ActiveSupport::TestCase 6 | setup do 7 | @uploader = CarrierWave::Uploader::Base.new 8 | @bucket = CarrierWave::Aliyun::Bucket.new(@uploader) 9 | end 10 | 11 | test "respond_to identifier, filename" do 12 | f = CarrierWave::Storage::AliyunFile.new(@uploader, "", "aaa/bbb.jpg") 13 | assert_equal "aaa/bbb.jpg", f.identifier 14 | assert_equal "aaa/bbb.jpg", f.filename 15 | assert_equal "jpg", f.extension 16 | end 17 | 18 | test "CJK file name" do 19 | f = rack_upload_file("中文 文件测试.zip", "application/zip") 20 | file_url = @bucket.put("/hello/中文 文件 100% 测试?a=1&b=2.zip", f, content_type: "application/zip") 21 | # puts "-------- #{file_url}" 22 | fname = File.basename(file_url) 23 | assert_equal "中文 文件 100% 测试?a=1&b=2.zip", CGI.unescape(fname) 24 | 25 | res = download_file(file_url) 26 | assert_equal "200", res.code 27 | assert_equal f.size, res.body.size 28 | assert_equal "application/zip", res["Content-Type"] 29 | end 30 | 31 | test "read work" do 32 | local_file = load_file("foo.jpg") 33 | image_url = @bucket.put("/a/a.jpg", load_file("foo.jpg")) 34 | res = download_file(image_url) 35 | 36 | @uploader.aliyun_mode = :public 37 | f = CarrierWave::Storage::AliyunFile.new(@uploader, "", "/a/a.jpg") 38 | body = f.read 39 | assert_equal res.body, body 40 | 41 | assert_equal "https://carrierwave-aliyun-test.oss-cn-beijing.aliyuncs.com/a/a.jpg", f.url 42 | %i[content_type server date content_length etag last_modified content_md5].each do |key| 43 | assert f.headers.key?(key) 44 | end 45 | assert_equal "image/jpg", f.content_type 46 | 47 | # copy_to 48 | new_file = f.copy_to("/a/a-copy.jpg") 49 | assert_equal true, new_file.exists? 50 | assert_equal "image/jpg", new_file.headers[:content_type] 51 | assert_equal local_file.size.to_s, new_file.headers[:content_length] 52 | assert_equal local_file.size, new_file.size 53 | assert_equal local_file.size, new_file.read.size 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | carrierwave-aliyun (1.2.3) 5 | aliyun-sdk (>= 0.7.0) 6 | carrierwave (>= 1) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activemodel (6.1.3) 12 | activesupport (= 6.1.3) 13 | activerecord (6.1.3) 14 | activemodel (= 6.1.3) 15 | activesupport (= 6.1.3) 16 | activesupport (6.1.3) 17 | concurrent-ruby (~> 1.0, >= 1.0.2) 18 | i18n (>= 1.6, < 2) 19 | minitest (>= 5.1) 20 | tzinfo (~> 2.0) 21 | zeitwerk (~> 2.3) 22 | addressable (2.8.0) 23 | public_suffix (>= 2.0.2, < 5.0) 24 | aliyun-sdk (0.8.0) 25 | nokogiri (~> 1.6) 26 | rest-client (~> 2.0) 27 | carrierwave (2.2.2) 28 | activemodel (>= 5.0.0) 29 | activesupport (>= 5.0.0) 30 | addressable (~> 2.6) 31 | image_processing (~> 1.1) 32 | marcel (~> 1.0.0) 33 | mini_mime (>= 0.1.3) 34 | ssrf_filter (~> 1.0) 35 | concurrent-ruby (1.1.9) 36 | domain_name (0.5.20190701) 37 | unf (>= 0.0.5, < 1.0.0) 38 | ffi (1.15.5) 39 | http-accept (1.7.0) 40 | http-cookie (1.0.3) 41 | domain_name (~> 0.5) 42 | i18n (1.8.11) 43 | concurrent-ruby (~> 1.0) 44 | image_processing (1.12.1) 45 | mini_magick (>= 4.9.5, < 5) 46 | ruby-vips (>= 2.0.17, < 3) 47 | marcel (1.0.2) 48 | mime-types (3.3.1) 49 | mime-types-data (~> 3.2015) 50 | mime-types-data (3.2021.0225) 51 | mini_magick (4.11.0) 52 | mini_mime (1.1.2) 53 | mini_portile2 (2.7.1) 54 | minitest (5.15.0) 55 | netrc (0.11.0) 56 | nokogiri (1.13.1) 57 | mini_portile2 (~> 2.7.0) 58 | racc (~> 1.4) 59 | public_suffix (4.0.6) 60 | racc (1.6.0) 61 | rack (2.2.3) 62 | rack-test (1.1.0) 63 | rack (>= 1.0, < 3) 64 | rake (13.0.6) 65 | rest-client (2.1.0) 66 | http-accept (>= 1.7.0, < 2.0) 67 | http-cookie (>= 1.0.2, < 2.0) 68 | mime-types (>= 1.16, < 4.0) 69 | netrc (~> 0.8) 70 | ruby-vips (2.1.4) 71 | ffi (~> 1.12) 72 | sqlite3 (1.4.2) 73 | ssrf_filter (1.0.7) 74 | tzinfo (2.0.4) 75 | concurrent-ruby (~> 1.0) 76 | unf (0.1.4) 77 | unf_ext 78 | unf_ext (0.0.7.7) 79 | zeitwerk (2.5.3) 80 | 81 | PLATFORMS 82 | ruby 83 | 84 | DEPENDENCIES 85 | activerecord 86 | carrierwave-aliyun! 87 | mini_magick 88 | minitest 89 | rack-test 90 | rake 91 | sqlite3 92 | 93 | BUNDLED WITH 94 | 2.2.3 95 | -------------------------------------------------------------------------------- /lib/carrierwave/storage/aliyun_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CarrierWave 4 | module Storage 5 | class AliyunFile 6 | attr_writer :file 7 | attr_reader :uploader, :path 8 | 9 | alias_method :filename, :path 10 | alias_method :identifier, :filename 11 | 12 | def initialize(uploader, base, path) 13 | @uploader = uploader 14 | @path = path 15 | @base = base 16 | end 17 | 18 | def file 19 | @file ||= bucket.get(path).try(:first) 20 | end 21 | 22 | def size 23 | file.headers[:content_length].to_i 24 | rescue 25 | nil 26 | end 27 | 28 | def read 29 | object, body = bucket.get(path) 30 | @headers = object.headers 31 | body 32 | end 33 | 34 | def delete 35 | bucket.delete(path) 36 | true 37 | rescue => e 38 | # If the file's not there, don't panic 39 | puts "carrierwave-aliyun delete file failed: #{e}" 40 | nil 41 | end 42 | 43 | ## 44 | # Generate file url 45 | # params 46 | # :thumb - Aliyun OSS Image Processor option, etc: @100w_200h_95q 47 | # 48 | def url(opts = {}) 49 | if bucket.mode == :private 50 | bucket.private_get_url(path, **opts) 51 | else 52 | bucket.path_to_url(path, **opts) 53 | end 54 | end 55 | 56 | def content_type 57 | headers[:content_type] 58 | end 59 | 60 | def content_type=(new_content_type) 61 | headers[:content_type] = new_content_type 62 | end 63 | 64 | def store(new_file, headers = {}) 65 | if new_file.is_a?(self.class) 66 | new_file.copy_to(path) 67 | else 68 | fog_file = new_file.to_file 69 | bucket.put(path, fog_file, **headers) 70 | fog_file.close if fog_file && !fog_file.closed? 71 | end 72 | true 73 | end 74 | 75 | def headers 76 | @headers ||= begin 77 | obj = bucket.head(path) 78 | obj.headers 79 | end 80 | end 81 | 82 | def exists? 83 | !!headers 84 | end 85 | 86 | def copy_to(new_path) 87 | bucket.copy_object(path, new_path) 88 | self.class.new(uploader, @base, new_path) 89 | end 90 | 91 | def extension 92 | path_elements = path.split(".") 93 | path_elements.last if path_elements.size > 1 94 | end 95 | 96 | def original_filename 97 | return @original_filename if @original_filename 98 | 99 | if @file&.respond_to?(:original_filename) 100 | @file.original_filename 101 | elsif path 102 | ::File.basename(path) 103 | end 104 | end 105 | 106 | private 107 | 108 | def bucket 109 | @bucket ||= CarrierWave::Aliyun::Bucket.new(uploader) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/aliyun_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class CarrierWave::Storage::AliyunTest < ActiveSupport::TestCase 6 | # test "store!" do 7 | # f = load_file("foo.jpg") 8 | # uploader = AttachUploader.new 9 | # uploader.store!(f) 10 | # 11 | # assert_match /\/attaches\//, uploader.url 12 | # attach = open(uploader.url) 13 | # assert_equal f.size, attach.size 14 | # end 15 | 16 | test "upload image" do 17 | @file = load_file("foo.jpg") 18 | @file1 = load_file("foo.gif") 19 | @photo = Photo.new(image: @file) 20 | @photo1 = Photo.new(image: @file1) 21 | 22 | assert_equal true, @photo.save! 23 | assert_equal "foo.jpg", @photo[:image] 24 | # assert_equal true, @photo.image? 25 | 26 | # FIXME: 不知为何,在 test 里面 @photo.image.url 如果不 reload 将会是 cache url 27 | # 而实际项目中没有这样的问题,这里强制 reload 避开 28 | assert_match "/photos/foo.jpg", @photo.image.url 29 | 30 | img = URI.open(@photo.image.url) 31 | assert_equal @file.size, img.size 32 | assert_equal "image/jpeg", img.content_type 33 | 34 | # get small version uploaded file 35 | assert_match "/photos/small_foo.jpg", @photo.image.small.url 36 | small_file = URI.open(@photo.image.small.url) 37 | assert_equal true, small_file.size > 0 38 | 39 | assert_equal true, @photo1.save! 40 | img1 = URI.open(@photo1.image.url) 41 | assert_equal @file1.size, img1.size 42 | assert_equal "image/gif", img1.content_type 43 | 44 | assert_no_cache_files @photo.image 45 | 46 | # get Aliyun OSS thumb url with :thumb option 47 | url = @photo.image.url(thumb: "?x-oss-process=image/resize,w_100") 48 | uri = URI.parse(url) 49 | assert_prefix_with "https://carrierwave-aliyun-test.oss-cn-beijing.aliyuncs.com", url 50 | assert_equal "x-oss-process=image%2Fresize%2Cw_100", uri.query 51 | 52 | url1 = @photo.image.url(thumb: "?x-oss-process=image/resize,w_60") 53 | uri = URI.parse(url1) 54 | assert_prefix_with "https://carrierwave-aliyun-test.oss-cn-beijing.aliyuncs.com", url1 55 | assert_equal "x-oss-process=image%2Fresize%2Cw_60", uri.query 56 | 57 | img1 = URI.open(url) 58 | assert_equal true, img1.size > 0 59 | assert_equal "image/jpeg", img1.content_type 60 | end 61 | 62 | test "upload CJK file name" do 63 | f = rack_upload_file("中文 文件测试.zip", "application/zip") 64 | attachment = Attachment.new(file: f) 65 | attachment.save! 66 | 67 | assert_no_cache_files attachment.file 68 | 69 | file_url = attachment.file.url 70 | # puts "-------- #{file_url}" 71 | res = download_file(file_url) 72 | assert_equal "200", res.code 73 | assert_equal f.size, res.body.size 74 | assert_equal "application/zip", res["Content-Type"] 75 | end 76 | 77 | test "upload a non image file" do 78 | f = load_file("foo.zip") 79 | attachment = Attachment.new(file: f) 80 | 81 | # should save 82 | attachment.save! 83 | 84 | # download and check response 85 | assert_match(%r{/attaches/}, attachment.file.url) 86 | 87 | attach = URI.open(attachment.file.url) 88 | assert_equal f.size, attach.size 89 | assert_equal "application/zip", attach.content_type 90 | assert_equal "attachment;filename=foo.zip", attach.meta["content-disposition"] 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require "sqlite3" 5 | require "active_record" 6 | require "carrierwave-aliyun" 7 | require "carrierwave/processing/mini_magick" 8 | require "open-uri" 9 | require "net/http" 10 | require "rack/test" 11 | 12 | module Rails 13 | class <= 0.7.0; 24 | 25 | ## 1.1.2 26 | 27 | - 修正废弃调用方式,避免在 Ruby 2.7 里面出现 warning. 28 | 29 | ## 1.1.1 30 | 31 | - 对于 CarrierWave 的 cache 机制正确支持; 32 | 33 | ## 1.1.0 34 | 35 | - 支持 CarrierWave 2.0; 36 | 37 | ## 1.0.0 38 | 39 | - 采用 Aliyun 官方的 SDK 来访问 OSS; 40 | - DEPRECATION: 配置参数命名规范化,老的配置方式将会在 1.1.0 版本废弃,请注意替换: 41 | - `aliyun_access_id` -> `aliyun_access_key_id` 42 | - `aliyun_access_key` -> `aliyun_access_key_secret` 43 | - `aliyun_area` -> `aliyun_region` 44 | - `aliyun_private_read` -> `aliyun_mode = :private` 45 | - 改进文件上传,支持 `chunk` 模式上传,提升大文件上传的效率以及降低内存开销; 46 | 47 | ## 0.9.0 48 | 49 | - 修正 `AliyunFile#read` 方法会报错的问题。(#53) 50 | 51 | ## 0.8.1 52 | 53 | - 去掉 `aliyun_img_host` 的配置项,不再需要了,Aliyun OSS 的 Bucket 域名以及默认执行图片处理协议,详见:[图片处理指南](https://help.aliyun.com/document_detail/44688.html). 54 | - 在用户未设置 `aliyun_host` 的时候,默认返回 https 协议的 aliyun 主机地址。(#42) 55 | 56 | ## 0.7.1 57 | 58 | - Fix Storage's read. (#41) 59 | 60 | ## 0.7.0 61 | 62 | - 支持设置 Content-Disposition; 63 | 64 | ## 0.6.0 65 | 66 | - 调整,优化类结构: 67 | - `CarrierWave::Storage::Aliyun::Connection` -> `CarrierWave::Aliyun::Bucket` 68 | - `CarrierWave::Storage::Aliyun::File` -> `CarrierWave::Storage::AliyunFile` 69 | 70 | ## 0.5.1 71 | 72 | - 修正 Aliyun 内部网络上传的支持;(#36) 73 | 74 | ## 0.5.0 75 | 76 | - 增加 Aliyun OSS 图片处理参数的支持,允许 url 传入 `:thumb` 以生成缩略图 URL; 77 | - 配置项增加 `config.aliyun_img_host`。 78 | 79 | ## 0.4.4 80 | 81 | - 修正对 Carrierwave master 版本的支持,它们[移除了](https://github.com/carrierwaveuploader/carrierwave/pull/1813) `carrierwave/processing/mime_types`; 82 | 83 | ## 0.4.3 84 | 85 | - 修正私密空间下载地址算法的问题,导致偶尔会签名错误无法下载的问题; 86 | 87 | ## 0.4.2 88 | 89 | - `config.aliyun_host` 现在支持配置 //you-host.com,以便同时支持 http 和 https。 90 | 91 | ## 0.4.1 92 | 93 | - 由于 aliyun-oss-sdk 目前不支持 internal 上传,暂时去掉,以免签名错误。 94 | 95 | ## 0.4.0 96 | 97 | - 采用 aliyun-oss-sdk 来作为上传后端,不再依赖 rest-client,不再内部实现上传逻辑; 98 | - 增加 `config.aliyun_private_read` 配置项,开启以后,返回的 @user.avatar.url 将会是带 Token 和有效期的 URL,可以用于访问私有读取空间的文件; 99 | - 去掉 `config.aliyun_upload_host` 配置项,删除了阿里内部的支持,以后请用 0.3.x 版本; 100 | 101 | ## 0.3.6 102 | 103 | - 修正上传中文文件名无法成功的问题; 104 | 105 | ## 0.3.5 106 | 107 | - CarrierWave::Storage::Aliyun::File 继承 CarrierWave::SanitizedFile 以实现一些方法; 108 | 109 | ## 0.3.4 110 | 111 | - Use OpenSSL::HMAC with Ruby 2.2.0. 112 | 113 | ## 0.3.3 114 | 115 | - 增加 `config.aliyun_upload_host`, 以便有需要的时候,可以自由修改上传的 host. 116 | 117 | ## 0.3.2 118 | 119 | - 请注意 `config.aliyun_host` 要求修改带 HTTP 协议,以便支持设置 http:// 或 https://. 120 | 121 | ## 0.3.1 122 | 123 | - 修复当文件名中包含了 "+",在 OSS 中上传会遇到签名不对应的问题; 124 | 125 | ## 0.3.0 126 | 127 | - 新增 `aliyun_area` 参数,用于配置 OSS 所在地区数据中心; 128 | 129 | ## 0.2.1 130 | 131 | - 避免计算上传文件的时候读取所有内容到内存,之前的做法对于大文件会耗费过多的内存; 132 | - Carrierwave::Storage::Aliyum::Connection 的 put 方法接口变化,file 现在应该传一个 File 的实例。 133 | 134 | ## 0.2.0 135 | 136 | - Aliyun OSS 新的[三级域名规则支持](http://bbs.aliyun.com/read.php?tid=139226) by [chaixl](https://github.com/chaixl) 137 | - 注意! 如果你之前使用 0.1.5 一下的版本,你可能需要调整一下你的自定义域名的 CNAME 解析,阿里云新的 URL 结构变化(少了 Bucket 一层目录),当然你也可以选择不要升级,之前 0.1.5 版本是稳定的。 138 | 139 | ## 0.1.5 140 | 141 | - 自定义域名支持 142 | 143 | ## 0.1.3 144 | 145 | - delete 接口加入。 146 | - 支持 Carriewave 自动在更新上传文件的时候删除老文件(比如,用户重新上传头像,老头像图片文件将会被 CarrierWave 删除)。 147 | 148 | ## 0.1.2 149 | 150 | - 修正 content_type 的支持,自动用原始文件的 content_type,以免上传 zip 之类的文件以后无法下载. 151 | 152 | ## 0.1.1 153 | 154 | - 修改 Aliyun OSS 的请求地址. 155 | - 加入可选项,使用 Aliyun 内部地址调用上传,以提高内部网络使用的速度. 156 | 157 | ## 0.1.0 158 | 159 | - 功能实现. 160 | -------------------------------------------------------------------------------- /test/bucket_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class CarrierWave::Aliyun::BucketTest < ActiveSupport::TestCase 6 | setup do 7 | @uploader = CarrierWave::Uploader::Base.new 8 | @bucket = CarrierWave::Aliyun::Bucket.new(@uploader) 9 | end 10 | 11 | test "base config" do 12 | assert_equal ALIYUN_ACCESS_KEY_ID, @bucket.access_key_id 13 | assert_equal ALIYUN_ACCESS_KEY_ID, @bucket.access_key_id 14 | assert_equal ALIYUN_ACCESS_KEY_SECRET, @bucket.access_key_secret 15 | assert_equal ALIYUN_BUCKET, @bucket.bucket 16 | assert_equal ALIYUN_REGION, @bucket.region 17 | assert_equal :public, @bucket.mode 18 | assert_equal "https://#{@bucket.bucket}.oss-#{@bucket.region}.aliyuncs.com", @bucket.host 19 | end 20 | 21 | test "put" do 22 | url = @bucket.put("a/a.jpg", load_file("foo.jpg")) 23 | res = download_file(url) 24 | assert_equal "200", res.code 25 | end 26 | 27 | test "put with / prefix" do 28 | url = @bucket.put("/a/a.jpg", load_file("foo.jpg")) 29 | res = download_file(url) 30 | assert_equal "200", res.code 31 | end 32 | 33 | test "with custom host" do 34 | @uploader.aliyun_host = "https://foo.bar.com" 35 | @bucket = CarrierWave::Aliyun::Bucket.new(@uploader) 36 | url = @bucket.put("a/a.jpg", load_file("foo.jpg")) 37 | assert_equal "https://foo.bar.com/a/a.jpg", url 38 | 39 | # get url 40 | assert_equal "https://foo.bar.com/foo/bar.jpg", @bucket.path_to_url("/foo/bar.jpg") 41 | assert_equal "https://foo.bar.com/foo/bar.jpg?x-oss-process=image%2Fresize%2Ch_100", @bucket.path_to_url("/foo/bar.jpg", thumb: "?x-oss-process=image/resize,h_100") 42 | assert_equal "https://foo.bar.com/foo/bar.jpg!sm", @bucket.path_to_url("/foo/bar.jpg", thumb: "!sm") 43 | assert_prefix_with "https://foo.bar.com/foo/bar.jpg", @bucket.private_get_url("/foo/bar.jpg") 44 | assert_prefix_with "https://foo.bar.com/foo/bar.jpg", @bucket.private_get_url("/foo/bar.jpg", thumb: "?x-oss-process=image/resize,h_100") 45 | 46 | @uploader.aliyun_host = "http://foo.bar.com" 47 | @bucket = CarrierWave::Aliyun::Bucket.new(@uploader) 48 | url = @bucket.put("a/a.jpg", load_file("foo.jpg")) 49 | assert_equal "http://foo.bar.com/a/a.jpg", url 50 | end 51 | 52 | test "delete" do 53 | url = @bucket.delete("/a/a.jpg") 54 | res = download_file(url) 55 | assert_equal "404", res.code 56 | end 57 | 58 | test "private mode" do 59 | @uploader.aliyun_mode = :private 60 | @uploader.aliyun_host = nil 61 | @bucket = CarrierWave::Aliyun::Bucket.new(@uploader) 62 | 63 | # should get url include token 64 | url = @bucket.private_get_url("bar/foo.jpg") 65 | # http://carrierwave-aliyun-test.oss-cn-beijing.aliyuncs.com/bar/foo.jpg?OSSAccessKeyId=1OpWEtPTjIDv5u8q&Expires=1455172009&Signature=4ibgQpfHOjVpqxG6162S8Ar3c6c= 66 | %w[Signature Expires OSSAccessKeyId].each do |key| 67 | assert_equal true, url.include?(key) 68 | end 69 | assert_prefix_with "https://#{@uploader.aliyun_bucket}.oss-#{@uploader.aliyun_region}.aliyuncs.com/bar/foo.jpg", url 70 | 71 | # should get url with :thumb 72 | url = @bucket.private_get_url("bar/foo.jpg", thumb: "?x-oss-process=image/resize,w_192,h_192,m_fill") 73 | assert_prefix_with "https://#{@uploader.aliyun_bucket}.oss-cn-beijing.aliyuncs.com/bar/foo.jpg?x-oss-process=image%2Fresize%2Cw_192%2Ch_192%2Cm_fill&Expires=", url 74 | end 75 | 76 | test "head" do 77 | f = load_file("foo.jpg") 78 | url = @bucket.put("foo/head-test.jpg", f) 79 | file = @bucket.head("foo/head-test.jpg") 80 | assert_kind_of Aliyun::OSS::Object, file 81 | assert_equal "foo/head-test.jpg", file.key 82 | assert_equal f.size, file.size 83 | end 84 | 85 | test "copy_object" do 86 | f = load_file("foo.jpg") 87 | url = @bucket.put("foo/source-test.jpg", f) 88 | @bucket.copy_object("foo/source-test.jpg", "foo/source-test-copy.jpg") 89 | 90 | file = @bucket.head("foo/source-test-copy.jpg") 91 | assert_kind_of Aliyun::OSS::Object, file 92 | assert_equal "foo/source-test-copy.jpg", file.key 93 | assert_equal f.size, file.size 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CarrierWave for Aliyun OSS 2 | 3 | This gem adds support for [Aliyun OSS](http://oss.aliyun.com) to [CarrierWave](https://github.com/jnicklas/carrierwave/) 4 | 5 | [![Gem Version](https://badge.fury.io/rb/carrierwave-aliyun.svg)](https://rubygems.org/gems/carrierwave-aliyun) [![build](https://github.com/huacnlee/carrierwave-aliyun/workflows/build/badge.svg)](https://github.com/huacnlee/carrierwave-aliyun/actions?query=workflow%3Abuild) 6 | 7 | > NOTE: 此 Gem 是一个 CarrierWave 的组件,你需要配合 CarrierWave 一起使用,如果你需要直接用 Aliyun OSS,可以尝试用 [aliyun-sdk](https://github.com/aliyun/aliyun-oss-ruby-sdk) 这个 Gem。 8 | 9 | > NOTE: This gem is a extends for [CarrierWave](https://github.com/jnicklas/carrierwave/) for allow it support use Alicloud OSS as storage backend, if you wants use Alicloud OSS directly, please visit [aliyun-sdk](https://github.com/aliyun/aliyun-oss-ruby-sdk). 10 | 11 | ## Using Bundler 12 | 13 | ```ruby 14 | gem 'carrierwave-aliyun' 15 | ``` 16 | 17 | ## Configuration 18 | 19 | You need a `config/initializers/carrierwave.rb` for initialize, and update your configurations: 20 | 21 | ```ruby 22 | CarrierWave.configure do |config| 23 | config.storage = :aliyun 24 | config.aliyun_access_key_id = "xxxxxx" 25 | config.aliyun_access_key_secret = 'xxxxxx' 26 | # 你需要在 Aliyun OSS 上面提前创建一个 Bucket 27 | # You must create a Bucket on Alicloud OSS first. 28 | config.aliyun_bucket = "simple" 29 | # 是否使用内部连接,true - 使用 Aliyun 主机内部局域网的方式访问 false - 外部网络访问 30 | # When your app server wants deployment in Alicloud internal network, enable this option can speed up uploading by using internal networking. otherwice you must disable it. 31 | config.aliyun_internal = true 32 | # 配置存储的地区数据中心,默认: "cn-hangzhou" 33 | # Which region of your Bucket. 34 | # config.aliyun_region = "cn-hangzhou" 35 | # 使用自定义域名,设定此项,carrierwave 返回的 URL 将会用自定义域名 36 |  # 自定义域名请 CNAME 到 you_bucket_name.oss-cn-hangzhou.aliyuncs.com (you_bucket_name 是你的 bucket 的名称) 37 | # aliyun_host allow you config a custom host for your Alicloud Bucket, and you also need config that on Alicloud. 38 | config.aliyun_host = "https://foo.bar.com" 39 | # Bucket 为私有读取请设置 true,默认 false,以便得到的 URL 是能带有 private 空间访问权限的逻辑 40 | # Tell SDK the privacy of you Bucket, if private CarrierWave xxx.url will generate URL with a expires parameter, default: :public. 41 | # config.aliyun_mode = :private 42 | end 43 | ``` 44 | 45 | ## 阿里云 OSS 图片缩略图 / About the image Thumb service for Alicloud OSS 46 | 47 | > NOTE: 此方法同样支持 Private 的 Bucket 哦! 48 | 49 | > NOTE: Private Bucket also support this feature! 50 | 51 | 关于阿里云 OSS 图片缩略图的详细文档,请仔细阅读:[Aliyun OSS 接入图片服务](https://help.aliyun.com/document_detail/44688.html) 52 | 53 | The details of the Alicoud OSS image thumb service, please visit [Alicloud OSS - Image Processing / Resize images](https://www.alibabacloud.com/help/doc-detail/44688.htm) 54 | 55 | ```rb 56 | irb> User.last.avatar.url(thumb: '?x-oss-process=image/resize,h_100') 57 | "https://simple.oss-cn-hangzhou.aliyuncs.com/users/avatar/12.png?x-oss-process=image/resize,h_100" 58 | irb> User.last.avatar.url(thumb: '?x-oss-process=image/resize,h_100,w_100') 59 | "https://simple.oss-cn-hangzhou.aliyuncs.com/users/avatar/12.png?x-oss-process=image/resize,h_100,w_100" 60 | ``` 61 | 62 | ## 增对文件设置 Content-Disposition / Customize the Content-Disposition 63 | 64 | 在文件上传的场景(非图片),你可能需要给上传的文件设置 `Content-Disposition` 以便于用户直接访问 URL 的时候能够用你期望的文件名或原文件名来下载并保存。 65 | 66 | In some case, you may need change the `Content-Disposition` for your uploaded files for allow users visit URL with direct download, and get the original filename. 67 | 68 | 这个时候你需要给 Uploader 实现 `content_disposition` 函数,例如: 69 | 70 | So, you need implement a `content_disposition` method for your CarrierWave Uploader, for example: 71 | 72 | ```rb 73 | # app/uploaders/attachment_uploader.rb 74 | class AttachmentUploader < CarrierWave::Uploader::Base 75 | def content_disposition 76 | # Only for non-image files 77 | unless file.extension.downcase.in?(%w(jpg jpeg gif png svg)) 78 | "attachment;filename=#{file.original_filename}" 79 | end 80 | end 81 | end 82 | ``` 83 | 84 | ## 启用全球传输加速 85 | 86 | 阿里云允许我们通过 `oss-accelerate.aliyuncs.com` 的节点来实现全球的传输加速,如果你的需要在境外的服务器传输到国内,或许需要开启这个功能。 87 | 88 | 你只需要将 CarrierWave Aliyun 的 `aliyun_region` 配置为 `accelerate` 即可。 89 | 90 | ```rb 91 | config.aliyun_region = "accelerate" 92 | ``` 93 | 94 | ### 异常解析 95 | 96 | > 错误:OSS Transfer Acceleration is not configured on this bucket. 97 | > 确保有开启 [传输加速](https://help.aliyun.com/document_detail/131312.html),进入 Bucket / 传输管理 / 传输加速 / 开启传输加速。 98 | 99 | 额外注意:Aliyun OSS 开启传输加速后需要 **30 分钟内全网生效** 100 | -------------------------------------------------------------------------------- /lib/carrierwave/aliyun/bucket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CarrierWave 4 | module Aliyun 5 | class Bucket 6 | PATH_PREFIX = %r{^/}.freeze 7 | CHUNK_SIZE = 1024 * 1024 8 | 9 | attr_reader :access_key_id, :access_key_secret, :bucket, :region, :mode, :host, :endpoint, :upload_endpoint, 10 | :get_endpoint 11 | 12 | def initialize(uploader) 13 | if uploader.aliyun_area.present? 14 | ActiveSupport::Deprecation.warn("config.aliyun_area will deprecation in carrierwave-aliyun 1.1.0, please use `aliyun_region` instead.") 15 | uploader.aliyun_region ||= uploader.aliyun_area 16 | end 17 | 18 | unless uploader.aliyun_private_read.nil? 19 | ActiveSupport::Deprecation.warn(%(config.aliyun_private_read will deprecation in carrierwave-aliyun 1.1.0, please use `aliyun_mode = :private` instead.)) 20 | uploader.aliyun_mode ||= uploader.aliyun_private_read ? :private : :public 21 | end 22 | 23 | if uploader.aliyun_access_id.present? 24 | ActiveSupport::Deprecation.warn(%(config.aliyun_access_id will deprecation in carrierwave-aliyun 1.1.0, please use `aliyun_access_key_id` instead.)) 25 | uploader.aliyun_access_key_id ||= uploader.aliyun_access_id 26 | end 27 | 28 | if uploader.aliyun_access_key.present? 29 | ActiveSupport::Deprecation.warn(%(config.aliyun_access_key will deprecation in carrierwave-aliyun 1.1.0, please use `aliyun_access_key_secret` instead.)) 30 | uploader.aliyun_access_key_secret ||= uploader.aliyun_access_key 31 | end 32 | 33 | @access_key_id = uploader.aliyun_access_key_id 34 | @access_key_secret = uploader.aliyun_access_key_secret 35 | @bucket = uploader.aliyun_bucket 36 | @region = uploader.aliyun_region || "cn-hangzhou" 37 | @mode = (uploader.aliyun_mode || :public).to_sym 38 | 39 | # Host for get request 40 | @endpoint = "https://#{bucket}.oss-#{region}.aliyuncs.com" 41 | @host = uploader.aliyun_host || @endpoint 42 | 43 | unless @host.include?("//") 44 | raise "config.aliyun_host requirement include // http:// or https://, but you give: #{host}" 45 | end 46 | 47 | @get_endpoint = "https://oss-#{region}.aliyuncs.com" 48 | @upload_endpoint = uploader.aliyun_internal == true ? "https://oss-#{region}-internal.aliyuncs.com" : "https://oss-#{region}.aliyuncs.com" 49 | end 50 | 51 | # 上传文件 52 | # params: 53 | # - path - remote 存储路径 54 | # - file - 需要上传文件的 File 对象 55 | # - opts: 56 | # - content_type - 上传文件的 MimeType,默认 `image/jpg` 57 | # - content_disposition - Content-Disposition 58 | # returns: 59 | # 图片的下载地址 60 | def put(path, file, content_type: "image/jpg", content_disposition: nil) 61 | path = path.sub(PATH_PREFIX, "") 62 | 63 | headers = {} 64 | headers["Content-Type"] = content_type 65 | headers["Content-Disposition"] = content_disposition if content_disposition 66 | 67 | oss_upload_client.put_object(path, headers: headers) do |stream| 68 | stream << file.read(CHUNK_SIZE) until file.eof? 69 | end 70 | path_to_url(path) 71 | end 72 | 73 | def copy_object(source, dest) 74 | source = source.sub(PATH_PREFIX, "") 75 | dest = dest.sub(PATH_PREFIX, "") 76 | 77 | oss_upload_client.copy_object(source, dest) 78 | end 79 | 80 | # 读取文件 81 | # params: 82 | # - path - remote 存储路径 83 | # returns: 84 | # file data 85 | def get(path) 86 | path = path.sub(PATH_PREFIX, "") 87 | chunk_buff = [] 88 | obj = oss_upload_client.get_object(path) do |chunk| 89 | chunk_buff << chunk 90 | end 91 | 92 | [obj, chunk_buff.join("")] 93 | end 94 | 95 | # 删除 Remote 的文件 96 | # 97 | # params: 98 | # - path - remote 存储路径 99 | # 100 | # returns: 101 | # 图片的下载地址 102 | def delete(path) 103 | path = path.sub(PATH_PREFIX, "") 104 | oss_upload_client.delete_object(path) 105 | path_to_url(path) 106 | end 107 | 108 | ## 109 | # 根据配置返回完整的上传文件的访问地址 110 | def path_to_url(path, thumb: nil) 111 | get_url(path, thumb: thumb) 112 | end 113 | 114 | # 私有空间访问地址,会带上实时算出的 token 信息 115 | # 有效期 15 minutes 116 | def private_get_url(path, thumb: nil) 117 | get_url(path, private: true, thumb: thumb) 118 | end 119 | 120 | def get_url(path, private: false, thumb: nil) 121 | path = path.sub(PATH_PREFIX, "") 122 | 123 | url = if thumb&.start_with?("?") 124 | # foo.jpg?x-oss-process=image/resize,h_100 125 | parameters = { "x-oss-process" => thumb.split("=").last } 126 | oss_client.object_url(path, private, 15.minutes, parameters) 127 | else 128 | oss_client.object_url(path, private, 15.minutes) 129 | end 130 | 131 | url = [url, thumb].join("") if !private && !thumb&.start_with?("?") 132 | 133 | url.sub(endpoint, host) 134 | end 135 | 136 | def head(path) 137 | path = path.sub(PATH_PREFIX, "") 138 | oss_upload_client.get_object(path) 139 | end 140 | 141 | # list_objects for test 142 | def list_objects(opts = {}) 143 | oss_client.list_objects(opts) 144 | end 145 | 146 | private 147 | 148 | def oss_client 149 | return @oss_client if defined?(@oss_client) 150 | 151 | client = ::Aliyun::OSS::Client.new(endpoint: get_endpoint, access_key_id: access_key_id, 152 | access_key_secret: access_key_secret) 153 | @oss_client = client.get_bucket(bucket) 154 | end 155 | 156 | def oss_upload_client 157 | return @oss_upload_client if defined?(@oss_upload_client) 158 | 159 | client = ::Aliyun::OSS::Client.new(endpoint: upload_endpoint, access_key_id: access_key_id, 160 | access_key_secret: access_key_secret) 161 | @oss_upload_client = client.get_bucket(bucket) 162 | end 163 | end 164 | end 165 | end 166 | --------------------------------------------------------------------------------