paperclip-backblaze 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 803ce541431482e546f6cf3b99c587444c5c423aa40314b0e97352e42959614f
4
+ data.tar.gz: dace72d3fe899c3407ac1b2ba1177b551fb43ea1ecded748810d7c3cd6ab2f99
5
+ SHA512:
6
+ metadata.gz: bb26137d390ba6c217fa389c71fc0ccf792fbebec22953fd1831e461cc4b9f9d80053e7f62f319647c19c19aea350c7ab3f5519b0cfd6e43a7533d778089e769
7
+ data.tar.gz: e966239867599f732adea915afb5df5f3183211dc3823b55905d56be50099ff11f62cbe9f3ad8d23510948ea1a9076d0485713f4c1ab6c2d9b4c1deb1f930635
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .DS_Store
11
+ .b2_login*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.2.2
5
+ - 2.4.2
6
+ before_install:
7
+ - rvm @global do gem install bundler -v 1.12.5
8
+
9
+ gemfile:
10
+ - Gemfile
11
+
12
+ script:
13
+ bundle exec rake
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Dockerfile ADDED
@@ -0,0 +1,9 @@
1
+ FROM ruby:2.4.2
2
+
3
+ ADD Gemfile* /srv/
4
+ ADD *.gemspec /srv/
5
+ WORKDIR /srv
6
+
7
+ RUN bundle install
8
+
9
+ ADD . /srv
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in backblaze.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Alex Tsui
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # paperclip-backblaze
2
+
3
+ [![Build Status](https://travis-ci.org/alextsui05/paperclip-backblaze.svg?branch=master)](https://travis-ci.org/alextsui05/paperclip-backblaze)
4
+
5
+ The `paperclip-backblaze` provides a [Paperclip](https://github.com/thoughtbot/paperclip) storage adapter so that
6
+ attachments can be saved to [Backblaze B2 Cloud Storage API](https://www.backblaze.com/b2/docs/).
7
+ It makes use of Winston Durand's [backblaze](https://github.com/R167/backblaze) gem
8
+ to access the B2 API behind the scenes.
9
+
10
+ Backblaze B2 Cloud Storage is similar to Amazon's AWS S3 Storage, but it has a few selling points:
11
+
12
+ 1. They run their own hardware (that's open-sourced, including the schematics, and drive reports)
13
+ 2. It's $0.005/GB/month vs S3's $0.030/GB/month
14
+ 3. You actually get a free GB of bandwidth a day, so it might be nice for personal projects.
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'paperclip-backblaze', github: 'alextsui05/paperclip-backblaze'
22
+ ```
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ ## Usage
28
+
29
+ You should be familiar with configuring Paperclip attachments for your model.
30
+ If not, please start with the Paperclip documentation
31
+ [here](https://github.com/thoughtbot/paperclip#usage).
32
+
33
+ Configuring Backblaze storage is very similar to [configuring S3 storage](http://www.rubydoc.info/gems/paperclip/Paperclip/Storage/S3).
34
+ Let's suppose we have a `Note` model with an `image` attachment that we would
35
+ like to be backed by Backblaze storage.
36
+
37
+
38
+
39
+ First, put your credentials and bucket name in a YML file.
40
+
41
+ ```.yml
42
+ # config/b2.yml
43
+ account_id: 123456789abc
44
+ application_key: 0123456789abcdef0123456789abcdef0123456789
45
+ bucket: my_b2_bucket_name
46
+ ```
47
+
48
+ Then let Paperclip know about it. You can put it in your environment config:
49
+
50
+ ```.rb
51
+ # config/environments/production.rb
52
+
53
+ ...
54
+ config.paperclip_defaults = {
55
+ storage: :backblaze,
56
+ b2_credentials: YAML.load(ERB.new(File.read("#{Rails.root}/config/b2.yml")).result).with_indifferent_access
57
+ }
58
+ ```
59
+
60
+ Now just specify the attachment in the model, and it will use your backblaze configuration:
61
+
62
+ ```.rb
63
+ # app/models/note.rb
64
+ class Note < ApplicationRecord
65
+ has_attached_file :image
66
+ ...
67
+ ```
68
+
69
+ Currently, these are required options:
70
+
71
+ - `:storage` - This should be set to :backblaze in order to use this
72
+ storage adapter.
73
+
74
+ - `:b2_credentials` - Should be a Hash containing required fields listend in config/b2.yml.
75
+
76
+ ## Contributing
77
+
78
+ This started as a proof of concept for a hobby project, so there's lots of room
79
+ for improvement, and it would be great to have your help.
80
+
81
+ Bug reports and pull requests are welcome on GitHub at
82
+ https://github.com/alextsui05/paperclip-backblaze. This project is intended to be a safe,
83
+ welcoming space for collaboration, and contributors are expected to adhere to
84
+ the [Contributor Covenant](contributor-covenant.org) code of conduct.
85
+
86
+ ## License
87
+
88
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
89
+
90
+ ## Version Log
91
+
92
+ ### 0.2.1
93
+
94
+ - CI setup and code style improvements
95
+
96
+ ### 0.2.0
97
+
98
+ - Remove `:b2_bucket` option. It should now be specified in the credentials file with a `bucket` key.
99
+ - Update example that doesn't pollute model with paperclip configuration
100
+
101
+ ### 0.1.0
102
+
103
+ First release
104
+
105
+
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
7
+
8
+ task :console do
9
+ require 'pry'
10
+ require 'backblaze'
11
+
12
+ def reload!
13
+ headers = Backblaze::B2::Base.headers
14
+ vars = Backblaze::B2.instance_variables.map { |k| [k, Backblaze::B2.instance_variable_get(k)] }.to_h
15
+ base_uri = Backblaze::B2::Base.base_uri
16
+ files = $LOADED_FEATURES.select { |feat| feat =~ /\/backblaze\// }
17
+ files.each { |file| load file }
18
+ vars.each do |key, value|
19
+ Backblaze::B2.instance_variable_set(key, value)
20
+ end
21
+ Backblaze::B2::Base.base_uri(base_uri)
22
+ Backblaze::B2::Base.headers(headers)
23
+ true
24
+ end
25
+
26
+ ARGV.clear
27
+ Pry.start
28
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'backblaze'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require 'pry'
11
+ Pry.start
12
+
13
+ # require "irb"
14
+ # IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,102 @@
1
+ module Backblaze::B2
2
+ class Base
3
+ include HTTParty
4
+ include Backblaze::Utils
5
+
6
+ format :json
7
+
8
+ # @!method get(path, options={}, &block)
9
+ # Calls the class level equivalent from HTTParty
10
+ # @see http://www.rubydoc.info/github/jnunemaker/httparty/HTTParty/ClassMethods HTTParty::ClassMethods
11
+
12
+ # @!method head(path, options={}, &block)
13
+ # (see #get)
14
+
15
+ # @!method post(path, options={}, &block)
16
+ # (see #get)
17
+
18
+ # @!method put(path, options={}, &block)
19
+ # (see #get)
20
+
21
+ %i[get head post put].each do |req|
22
+ define_method(req) do |path, options = {}, &block|
23
+ self.class.send(req, path, options, &block)
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def file_versions(bucket_id:, convert:, limit:, double_check_server:, file_name: nil, &block)
30
+ retreive_count = (double_check_server ? 0 : -1)
31
+ files = file_list(bucket_id: bucket_id, limit: limit, retreived: retreive_count, file_name: file_name, first_file: nil, start_field: 'startFileId'.freeze)
32
+
33
+ if convert
34
+ files.map! do |f|
35
+ if block.nil?
36
+ Backblaze::B2::FileVersion.new(f)
37
+ else
38
+ yield(f)
39
+ end
40
+ end
41
+ end
42
+ files.compact
43
+ end
44
+
45
+ def file_list(limit:, retreived:, first_file:, start_field:, bucket_id:, file_name: nil, first: true)
46
+ params = { 'bucketId'.freeze => bucket_id }
47
+ if limit == -1
48
+ params['maxFileCount'.freeze] = 1000
49
+ elsif limit > 1000
50
+ params['maxFileCount'.freeze] = 1000
51
+ elsif limit > 0
52
+ params['maxFileCount'.freeze] = limit
53
+ else
54
+ return []
55
+ end
56
+ if first_file.nil?
57
+ if file_name && start_field == 'startFileId' && first
58
+ params['startFileName'] = file_name
59
+ end
60
+ else
61
+ params[start_field] = first_file
62
+ end
63
+
64
+ response = post("/b2_list_file_#{start_field == 'startFileName' ? 'names' : 'versions'}", body: params.to_json)
65
+
66
+ raise Backblaze::FileError, response unless response.code == 200
67
+
68
+ files = response['files'.freeze]
69
+ halt = false
70
+ files.map! do |f|
71
+ if halt
72
+ nil
73
+ else
74
+ ret = Hash[f.map { |k, v| [Backblaze::Utils.underscore(k).to_sym, v] }]
75
+ halt = true if file_name && file_name != ret[:file_name]
76
+ halt ? nil : ret
77
+ end
78
+ end.compact!
79
+
80
+ retreived += files.size if retreived >= 0
81
+ if limit > 0
82
+ limit -= (retreived >= 0 ? files.size : 1000)
83
+ limit = 0 if limit < 0
84
+ end
85
+
86
+ next_item = response[start_field.sub('start'.freeze, 'next'.freeze)]
87
+
88
+ if (limit > 0 || limit == -1) && !!next_item && !halt
89
+ files.concat file_list(
90
+ first_file: next_item,
91
+ limit: limit,
92
+ retreived: retreived,
93
+ start_field: start_field,
94
+ bucket_id: bucket_id,
95
+ first: false
96
+ )
97
+ else
98
+ files
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,156 @@
1
+ module Backblaze::B2
2
+ ##
3
+ # A class to represent the online buckets. Mostly used for file access
4
+ class Bucket < Base
5
+ ##
6
+ # Creates a bucket from all of the possible parameters. This sould be rarely used and instead use a finder or creator
7
+ # @param [#to_s] bucket_name the bucket name
8
+ # @param [#to_s] bucket_id the bucket id
9
+ # @param [#to_s] bucket_type the bucket publicity type
10
+ # @param [#to_s] account_id the account to which this bucket belongs
11
+ def initialize(options)
12
+ @bucket_name = options.fetch(:bucket_name)
13
+ @bucket_id = options.fetch(:bucket_id)
14
+ @bucket_type = options.fetch(:bucket_type)
15
+ @account_id = options.fetch(:account_id)
16
+ end
17
+
18
+ # @return [String] bucket name
19
+ attr_reader :bucket_name
20
+
21
+ alias name bucket_name
22
+
23
+ # @return [String] bucket id
24
+ attr_reader :bucket_id
25
+
26
+ # @return [Boolean] is the bucket public
27
+ def public?
28
+ @bucket_type == 'allPublic'
29
+ end
30
+
31
+ # @return [Boolean] is the bucket private
32
+ def private?
33
+ !public?
34
+ end
35
+
36
+ # @return [String] account id
37
+ attr_reader :account_id
38
+
39
+ # @return [String] bucket type
40
+ attr_reader :bucket_type
41
+
42
+ # Check if eqivalent. Takes advantage of globally unique names
43
+ # @return [Boolean] equality
44
+ def ==(other)
45
+ bucket_name == other.bucket_name
46
+ end
47
+
48
+ ##
49
+ # Lists all files that are in the bucket. This is the basic building block for the search.
50
+ # @param [String] first_file first file in the bucket to start listing from
51
+ # @param [Integer] limit max number of files to retreive. Set to `-1` to get all files.
52
+ # This is not exact as it mainly just throws the limit into max param on the request
53
+ # so it will try to grab at least `limit` files, unless there aren't enoungh in the bucket
54
+ # @param [Boolean] cache if there is no cache, create one. If there is a cache, use it.
55
+ # Will check if the previous cache had the same size limit and convert options
56
+ # @param [Boolean] convert convert the files to Backblaze::B2::File objects
57
+ # @param [Integer] double_check_server whether or not to assume the server returns the most files possible
58
+ # @return [Array<Backblaze::B2::File>] when convert is true
59
+ # @return [Array<Hash>] when convert is false
60
+ # @note many of these methods are for the recusion
61
+ def file_names(first_file: nil, limit: 100, cache: false, convert: true, double_check_server: false)
62
+ if cache && !@file_name_cache.nil?
63
+ if limit <= @file_name_cache[:limit] && convert == @file_name_cache[:convert]
64
+ return @file_name_cache[:files]
65
+ end
66
+ end
67
+
68
+ retreive_count = (double_check_server ? 0 : -1)
69
+ files = file_list(bucket_id: bucket_id, limit: limit, retreived: retreive_count, first_file: first_file, start_field: 'startFileName'.freeze)
70
+
71
+ merge_params = { bucket_id: bucket_id }
72
+ if convert
73
+ files.map! do |f|
74
+ Backblaze::B2::File.new(f.merge(merge_params))
75
+ end
76
+ end
77
+ if cache
78
+ @file_name_cache = { limit: limit, convert: convert, files: files }
79
+ end
80
+ files
81
+ end
82
+
83
+ def file_versions(limit: 100, cache: false, convert: true, double_check_server: false)
84
+ if cache && !@file_versions_cache.nil?
85
+ if limit <= @file_versions_cache[:limit] && convert == @file_versions_cache[:convert]
86
+ return @file_versions_cache[:files]
87
+ end
88
+ end
89
+ file_versions = super(limit: 100, convert: convert, double_check_server: double_check_server, bucket_id: bucket_id)
90
+ files = file_versions.group_by { |version| convert ? version.file_name : version[:file_name] }
91
+ if convert
92
+ files = files.map do |name, versions|
93
+ File.new(file_name: name, bucket_id: bucket_id, versions: versions)
94
+ end
95
+ end
96
+ @file_versions_cache = if cache
97
+ { limit: limit, convert: convert, files: files }
98
+ else
99
+ {}
100
+ end
101
+ files
102
+ end
103
+
104
+ def upload_url
105
+ self.class.upload_url(bucket_id: bucket_id)
106
+ end
107
+
108
+ class << self
109
+ ##
110
+ # Create a bucket
111
+ # @param [String] name name of the new bucket
112
+ # must be no more than 50 character and only contain letters, digits, "-", and "_".
113
+ # must be globally unique
114
+ # @param [:public, :private] type determines the type of bucket
115
+ # @raise [Backblaze::BucketError] unable to create the specified bucket
116
+ def create(name:, type:)
117
+ body = {
118
+ accountId: Backblaze::B2.account_id,
119
+ bucketName: name,
120
+ bucketType: (type == :public ? 'allPublic' : 'allPrivate')
121
+ }
122
+ response = post('/b2_create_bucket', body: body.to_json)
123
+
124
+ raise Backblaze::BucketError, response unless response.code / 100 == 2
125
+
126
+ params = Hash[response.map { |k, v| [Backblaze::Utils.underscore(k).to_sym, v] }]
127
+
128
+ new(params)
129
+ end
130
+
131
+ def upload_url(bucket_id:)
132
+ response = post('/b2_get_upload_url', body: { bucketId: bucket_id }.to_json)
133
+ raise Backblaze::BucketError, response unless response.code / 100 == 2
134
+ { url: response['uploadUrl'], token: response['authorizationToken'] }
135
+ end
136
+
137
+ ##
138
+ # List buckets for account
139
+ # @return [Array<Backblaze::Bucket>] buckets for this account
140
+ def buckets
141
+ body = {
142
+ accountId: Backblaze::B2.account_id
143
+ }
144
+ response = post('/b2_list_buckets', body: body.to_json)
145
+ response['buckets'].map do |bucket|
146
+ params = Hash[bucket.map { |k, v| [Backblaze::Utils.underscore(k).to_sym, v] }]
147
+ new(params)
148
+ end
149
+ end
150
+
151
+ def get_bucket(name:)
152
+ buckets.find { |b| b.bucket_name == name }
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,173 @@
1
+ module Backblaze::B2
2
+ class File < Base
3
+ def initialize(file_name:, bucket_id:, versions: nil, **file_version_args)
4
+ @file_name = file_name
5
+ @bucket_id = bucket_id
6
+ if versions
7
+ @fetched_all = true
8
+ @versions = versions
9
+ else
10
+ @fetched_all = false
11
+ @versions = [FileVersion.new(file_version_args.merge(file_name: file_name))]
12
+ end
13
+ end
14
+
15
+ class << self
16
+ def create(data:, bucket:, name: nil, base_name: '', content_type: 'b2/x-auto', info: {})
17
+ raise ArgumentError, 'data must not be nil' if data.nil?
18
+
19
+ case bucket
20
+ when String
21
+ upload_url = Bucket.upload_url(bucket_id: bucket)
22
+ when Bucket
23
+ upload_url = bucket.upload_url
24
+ else
25
+ raise ArgumentError, 'You must pass a bucket'
26
+ end
27
+
28
+ case data
29
+ when String
30
+ data.force_encoding('ASCII-8BIT')
31
+ raise ArgumentError, 'Must provide a file name for data' if name.nil?
32
+ when ::File, Tempfile, ::Paperclip::UploadedFileAdapter
33
+ data.binmode
34
+ data.rewind
35
+ if name.nil?
36
+ raise ArgumentError, 'Must provide a file name with Tempfiles' if data.is_a? Tempfile
37
+ name = ::File.basename(data)
38
+ end
39
+ else
40
+ raise ArgumentError, 'Must provide a file name with streams' if name.nil?
41
+ if data.respond_to?(:read)
42
+ temp = Tempfile.new(name)
43
+ temp.binmode
44
+ IO.copy_stream(data, temp)
45
+ data = temp
46
+ data.rewind
47
+ else
48
+ raise ArgumentError, 'Unsuitable data type. Please read the docs.'
49
+ end
50
+ end
51
+
52
+ uri = URI(upload_url[:url])
53
+ req = Net::HTTP::Post.new(uri)
54
+
55
+ req.add_field('Authorization', upload_url[:token])
56
+ req.add_field('X-Bz-File-Name', "#{base_name}/#{name}".tr_s('/', '/').sub(/\A\//, ''))
57
+ req.add_field('Content-Type', content_type)
58
+ req.add_field('Content-Length', data.size)
59
+
60
+ digest = Digest::SHA1.new
61
+ if data.is_a? String
62
+ digest.update(data)
63
+ req.body = data
64
+ elsif data.is_a? ::Paperclip::UploadedFileAdapter
65
+ digest.file(data.path)
66
+ data.rewind
67
+ req.body_stream = data
68
+ else
69
+ digest.file(data)
70
+ data.rewind
71
+ req.body_stream = data
72
+ end
73
+
74
+ req.add_field('X-Bz-Content-Sha1', digest)
75
+
76
+ info.first(10).map do |key, value|
77
+ encoded_key = URI.encode_www_form_component(key)
78
+ req.add_field("X-Bz-Info-#{encoded_key}", value)
79
+ end
80
+
81
+ http = Net::HTTP.new(req.uri.host, req.uri.port)
82
+ http.use_ssl = (req.uri.scheme == 'https')
83
+ res = http.start { |make| make.request(req) }
84
+
85
+ response = JSON.parse(res.body)
86
+
87
+ raise Backblaze::FileError, response unless res.code.to_i == 200
88
+
89
+ params = {
90
+ file_name: response['fileName'],
91
+ bucket_id: response['bucketId'],
92
+ size: response['contentLength'],
93
+ file_id: response['fileId'],
94
+ upload_timestamp: Time.now.to_i * 1000,
95
+ content_length: data.size,
96
+ content_type: content_type,
97
+ content_sha1: digest,
98
+ action: 'upload'
99
+ }
100
+
101
+ File.new(params)
102
+ end
103
+ end
104
+
105
+ attr_reader :file_name
106
+ alias name file_name
107
+
108
+ def versions
109
+ unless @fetched_all
110
+ @versions = file_versions(bucket_id: @bucket_id, convert: true, limit: -1, double_check_server: false, file_name: file_name)
111
+ @fetched_all = true
112
+ end
113
+ @versions
114
+ end
115
+
116
+ def download_url(bucket:)
117
+ "#{Backblaze::B2.download_url}/file/#{bucket.is_a?(Bucket) ? bucket.name : bucket}/#{file_name}"
118
+ end
119
+
120
+ def file_id_download_url
121
+ latest.download_url
122
+ end
123
+
124
+ def latest
125
+ @versions.first
126
+ end
127
+
128
+ def destroy!(thread_count: 4)
129
+ versions
130
+ thread_count = @versions.length if thread_count > @versions.length || thread_count < 1
131
+ lock = Mutex.new
132
+ errors = []
133
+ threads = []
134
+ thread_count.times do
135
+ threads << Thread.new do
136
+ version = nil
137
+ loop do
138
+ lock.synchronize { version = @versions.pop }
139
+ break if version.nil?
140
+ begin
141
+ version.destroy!
142
+ rescue Backblaze::FileError => e
143
+ lock.synchronize { errors << e }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ threads.map(&:join)
149
+ @destroyed = true
150
+ raise Backblaze::DestroyErrors, errors if errors.any?
151
+ end
152
+
153
+ def exists?
154
+ !@destroyed
155
+ end
156
+
157
+ def method_missing(m, *args, &block)
158
+ if latest.respond_to?(m)
159
+ latest.send(m, *args, &block)
160
+ else
161
+ super
162
+ end
163
+ end
164
+
165
+ def respond_to?(m)
166
+ if latest.respond_to?(m)
167
+ true
168
+ else
169
+ super
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,43 @@
1
+ module Backblaze::B2
2
+ class FileVersion < Base
3
+ attr_reader :file_id, :size, :action, :upload_timestamp, :file_name,
4
+ :content_length, :content_sha1, :content_type, :file_info
5
+
6
+ def initialize(file_id:, size:, upload_timestamp:, action:, file_name:,
7
+ content_length:, content_sha1:, content_type:, file_info: {}, **unneeded_args)
8
+ @file_id = file_id
9
+ @size = size
10
+ @action = action
11
+ @file_name = file_name
12
+ @content_length = content_length
13
+ @content_sha1 = content_sha1
14
+ @content_type = content_type
15
+ @file_info = file_info
16
+ @upload_timestamp = Time.at(upload_timestamp / 1000.0)
17
+ end
18
+
19
+ def get_info
20
+ unless defined?(@get_info)
21
+ response = post('/b2_get_file_info', body: { fileId: file_id }.to_json)
22
+ raise Backblaze::FileError, response unless response.code == 200
23
+
24
+ @get_info = Hash[response.map { |k, v| [Backblaze::Utils.underscore(k).to_sym, v] }]
25
+ end
26
+ @get_info
27
+ end
28
+
29
+ def download_url
30
+ "#{Backblaze::B2.download_url}#{Backblaze::B2.api_path}b2_download_file_by_id?fileId=#{file_id}"
31
+ end
32
+
33
+ def destroy!
34
+ response = post('/b2_delete_file_version', body: { fileName: file_name, fileId: file_id }.to_json)
35
+ raise Backblaze::FileError, response unless response.code == 200
36
+ @destroyed = true
37
+ end
38
+
39
+ def exists?
40
+ !@destroyed
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ require 'backblaze/b2/base'
2
+ require 'backblaze/b2/bucket'
3
+ require 'backblaze/b2/file'
4
+ require 'backblaze/b2/file_version'
5
+ require 'tempfile'
6
+ require 'digest/sha1'
7
+ require 'base64'
8
+
9
+ module Backblaze::B2
10
+ class << self
11
+ attr_reader :account_id, :token, :api_url, :download_url, :api_path
12
+
13
+ ##
14
+ # Authenticates with the server to get the authorization data. Raises an error if there is a problem
15
+ #
16
+ # @param [#to_s] account_id the account id
17
+ # @param [#to_s] application_key the private app key
18
+ # @raise [Backblaze::AuthError] when unable to authenticate
19
+ # @return [void]
20
+ def login(options)
21
+ api_path = options.fetch(:api_path) { '/b2api/v1/' }
22
+
23
+ params = {
24
+ headers: {
25
+ 'Content-Type' => 'application/json',
26
+ 'Accept' => 'application/json',
27
+ 'Authorization' => bearer(options)
28
+ }
29
+ }
30
+ api_path = "/#{api_path}/".gsub(/\/+/, '/')
31
+
32
+ response = HTTParty.get("https://api.backblazeb2.com#{api_path}b2_authorize_account", params)
33
+
34
+ raise Backblaze::AuthError, response unless response.success?
35
+
36
+ @account_id = response['accountId']
37
+ @token = response['authorizationToken']
38
+ @api_url = response['apiUrl']
39
+ @download_url = response['downloadUrl']
40
+ @api_path = api_path
41
+ Backblaze::B2::Base.base_uri("#{@api_url}#{api_path}")
42
+ Backblaze::B2::Base.headers('Authorization' => token,
43
+ 'Content-Type' => 'application/json')
44
+ end
45
+
46
+ def bearer(options)
47
+ account_id = options.fetch(:account_id)
48
+ application_key = options.fetch(:application_key)
49
+ token = Base64.strict_encode64("#{account_id}:#{application_key}")
50
+
51
+ "Basic #{token}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,80 @@
1
+ module Backblaze
2
+ ##
3
+ # Base Backblaze error class
4
+ # @abstract
5
+ class Error < StandardError
6
+ end
7
+
8
+ ##
9
+ # Basic needs for error messages.
10
+ # @note this could be abstract, but just keeps things simple.
11
+ class RequestError < Error
12
+ ##
13
+ # Creates the Error
14
+ # @param [HTTParty::Response] response the json response
15
+ def initialize(response)
16
+ @response = response
17
+ end
18
+
19
+ ##
20
+ # The response from the server
21
+ # @return [HTTParty::Response] the response
22
+ attr_reader :response
23
+
24
+ ##
25
+ # The Backblaze B2 error code
26
+ # @return [String] error code
27
+ def code
28
+ self['code']
29
+ end
30
+
31
+ ##
32
+ # The Backblaze B2 request status
33
+ # @return [Integer] status code
34
+ def status
35
+ self['status']
36
+ end
37
+
38
+ ##
39
+ # The Backblaze B2 error message which is a human explanation
40
+ # @return [String] the problem in human words
41
+ def message
42
+ self['message']
43
+ end
44
+
45
+ ##
46
+ # Shortcut to access the response keys
47
+ # @return [Object] the object stored at `key` in the response
48
+ def [](key)
49
+ @response[key]
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Errors destroying file versions
55
+ class DestroyErrors < Error
56
+ ##
57
+ # Creates the Error
58
+ # @param [Array<Backblaze::FileError>] errors errors raised destroying files
59
+ def initialize(errors)
60
+ @errors = errors
61
+ end
62
+
63
+ ##
64
+ # The Backblaze B2 error messages which broke things
65
+ # @return [Array<Backblaze::FileError>] errors errors raised destroying files
66
+ attr_reader :errors
67
+ end
68
+
69
+ ##
70
+ # Error class for authentication errors
71
+ class AuthError < RequestError; end
72
+
73
+ ##
74
+ # Error class for bucket errors
75
+ class BucketError < RequestError; end
76
+
77
+ ##
78
+ # Error class for file errors
79
+ class FileError < RequestError; end
80
+ end
@@ -0,0 +1,26 @@
1
+ module Backblaze::Utils
2
+ def underscore(word)
3
+ word.to_s
4
+ .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
5
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
6
+ .tr('-', '_')
7
+ .downcase
8
+ end
9
+
10
+ def camelize(word, capitalize = false)
11
+ word = word.to_s
12
+ "#{capitalize ? word[0, 1].upcase : word[0, 1].downcase}#{word.split('_').map(&:capitalize).join('')[1..-1]}"
13
+ end
14
+
15
+ def self.included(base)
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ include Backblaze::Utils
21
+ end
22
+
23
+ class << self
24
+ include Backblaze::Utils
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Backblaze
2
+ VERSION = '0.3.0'.freeze
3
+ end
data/lib/backblaze.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'httparty'
2
+
3
+ require 'backblaze/version'
4
+ require 'backblaze/utils'
5
+ require 'backblaze/errors'
6
+ require 'backblaze/b2'
7
+
8
+ module Backblaze
9
+ end
@@ -0,0 +1,5 @@
1
+ module Paperclip
2
+ module Backblaze
3
+ VERSION = '0.2.2'.freeze
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ require_relative '../backblaze'
2
+
3
+ require 'paperclip/backblaze/version'
4
+ require 'paperclip/storage/backblaze'
@@ -0,0 +1,142 @@
1
+ module Paperclip
2
+ module Storage
3
+ # Defines a adapter to use store Paperclip attachments to Backblaze B2
4
+ # Cloud Storage, which is similar to Amazon's S3 service.
5
+ #
6
+ # This allows you to use has_attached_file in your models
7
+ # with :storage => :backblaze.
8
+ #
9
+ # Some required options include:
10
+ #
11
+ # :storage - This should be set to :backblaze in order to use this
12
+ # storage adapter.
13
+ #
14
+ # :b2_credentials - This should point to a YAML file containing your B2
15
+ # account ID, application key, and bucket. The contents should look
16
+ # something like:
17
+ #
18
+ # account_id: 123456789abc
19
+ # application_key: 0123456789abcdef0123456789abcdef0123456789
20
+ # bucket: my_b2_bucket_name
21
+ #
22
+ # So for example, a model might be configured something like this:
23
+ #
24
+ # class Note < ApplicationRecord
25
+ # has_attached_file :image,
26
+ # storage: :backblaze,
27
+ # b2_credentials: YAML.safe_load(ERB.new(File.read("#{Rails.root}/config/back_blaze.yml")).result)[Rails.env],
28
+ # ...
29
+ #
30
+ # You can put the backblaze config in the environment config file e.g.:
31
+ #
32
+ # # config/environments/production.rb
33
+ #
34
+ # config.paperclip_defaults = {
35
+ # storage: :backblaze,
36
+ # b2_credentials: [Hash]
37
+ # }
38
+ #
39
+ # If you do it this way, then you don't need to put it in the model.
40
+ #
41
+ module Backblaze
42
+ def self.extended(base)
43
+ base.instance_eval do
44
+ @b2_buckets = {}
45
+ login
46
+ unless @options[:url] =~ /\Ab2.*url\z/
47
+ @options[:url] = ':b2_path_url'.freeze
48
+ end
49
+ end
50
+
51
+ unless Paperclip::Interpolations.respond_to? :b2_path_url
52
+ Paperclip.interpolates(:b2_path_url) do |attachment, style|
53
+ "#{::Backblaze::B2.download_url}/file/#{attachment.b2_bucket_name}/#{attachment.path(style).sub(%r{\A/}, '')}"
54
+ end
55
+ end
56
+ end
57
+
58
+ # Reads b2_credentials from the config file
59
+ # must be a hash
60
+ # {
61
+ # account_id: 123456789abc
62
+ # application_key: 0123456789abcdef0123456789abcdef0123456789
63
+ # }
64
+ # Returns a Hash containing the parsed credentials.
65
+ def b2_credentials
66
+ @b2_credentials ||= @options.fetch(:b2_credentials)
67
+ end
68
+
69
+ # Authenticate with Backblaze with the account ID and secret key. This
70
+ # also caches several variables from the response related to the API, so
71
+ # it is important that it is executed at the very beginning.
72
+ def login
73
+ return if ::Backblaze::B2.token
74
+ creds = b2_credentials
75
+ ::Backblaze::B2.login(
76
+ account_id: creds.fetch(:account_id),
77
+ application_key: creds.fetch(:application_key)
78
+ )
79
+ end
80
+
81
+ # Return the Backblaze::B2::Bucket object representing the bucket
82
+ # specified by the required options[:b2_bucket].
83
+ def b2_bucket
84
+ bucket_name = @options[:bucket] || b2_credentials.fetch(:bucket)
85
+ @b2_buckets[bucket_name] ||= ::Backblaze::B2::Bucket.get_bucket(
86
+ name: bucket_name
87
+ )
88
+ end
89
+
90
+ # Return the specified bucket name as a String.
91
+ def b2_bucket_name
92
+ b2_bucket.bucket_name
93
+ end
94
+
95
+ # Return whether this attachment exists in the bucket.
96
+ def exists?(style = default_style)
97
+ !get_file(filename: get_path(style)).nil?
98
+ end
99
+
100
+ # Return a Backblaze::B2::File object representing the file named in the
101
+ # filename keyword, if it exists.
102
+ def get_file(filename:)
103
+ b2_bucket.file_names(first_file: filename, limit: 1).find do |f|
104
+ f.file_name == filename
105
+ end
106
+ end
107
+
108
+ # Return this attachment's bucket file path as a String.
109
+ def get_path(style = default_style)
110
+ path(style).sub(%r{\A/}, '')
111
+ end
112
+
113
+ # (Internal) Used by Paperclip to upload local files to storage.
114
+ def flush_writes
115
+ @queued_for_write.each do |style, file|
116
+ base_name = ::File.dirname(get_path(style))
117
+ name = ::File.basename(get_path(style))
118
+ ::Backblaze::B2::File.create data: file, bucket: b2_bucket, name: name, base_name: base_name
119
+ end
120
+ @queued_for_write = {}
121
+ end
122
+
123
+ # (Internal) Used by Paperclip to remove remote files from storage.
124
+ def flush_deletes
125
+ @queued_for_delete.each do |path|
126
+ file = get_file(filename: path.sub(%r{\A/}, ''))
127
+ file.destroy! unless file.nil?
128
+ end
129
+ @queued_for_delete = []
130
+ end
131
+
132
+ # (Internal)
133
+ def copy_to_local_file(style, local_dest_path)
134
+ ::File.open(local_dest_path, 'wb') do |local_file|
135
+ file = get_file(filename: get_path(style))
136
+ body = file.get(file.latest.download_url, parse: :plain)
137
+ local_file.write(body)
138
+ end
139
+ end
140
+ end # module Backblaze
141
+ end # module Storage
142
+ end # module Paperclip
@@ -0,0 +1,28 @@
1
+
2
+ Gem::Specification.new do |spec|
3
+ spec.name = 'paperclip-backblaze'
4
+ spec.version = '0.2.4'
5
+ spec.authors = ['Alex Tsui', 'Winston Durand']
6
+ spec.email = ['alextsui05@gmail.com']
7
+
8
+ spec.summary = 'Paperclip storage adapter for Backblaze B2 Cloud.'
9
+ spec.description = 'Allows Paperclip attachments to be backed by Backblaze B2 Cloud storage as an alternative to AWS S3.'
10
+ spec.homepage = 'https://github.com/alextsui05/paperclip-backblaze'
11
+ spec.license = 'MIT'
12
+
13
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
14
+ spec.bindir = 'bin'
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_development_dependency 'bundler', '~> 1.10'
19
+ spec.add_development_dependency 'pry'
20
+ spec.add_development_dependency 'rake', '~> 12.0'
21
+ spec.add_development_dependency 'rspec'
22
+ spec.add_development_dependency 'rubocop', '~> 0.51.0'
23
+ spec.add_development_dependency 'webmock'
24
+ spec.add_development_dependency 'yard'
25
+ spec.add_dependency 'httparty'
26
+
27
+ spec.required_ruby_version = '>= 2.1.0'
28
+ end
metadata ADDED
@@ -0,0 +1,183 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paperclip-backblaze
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ platform: ruby
6
+ authors:
7
+ - Alex Tsui
8
+ - Winston Durand
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2021-12-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: pry
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '12.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '12.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rubocop
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 0.51.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 0.51.0
84
+ - !ruby/object:Gem::Dependency
85
+ name: webmock
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: yard
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: httparty
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Allows Paperclip attachments to be backed by Backblaze B2 Cloud storage
127
+ as an alternative to AWS S3.
128
+ email:
129
+ - alextsui05@gmail.com
130
+ executables:
131
+ - console
132
+ - setup
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - ".gitignore"
137
+ - ".rspec"
138
+ - ".travis.yml"
139
+ - CODE_OF_CONDUCT.md
140
+ - Dockerfile
141
+ - Gemfile
142
+ - LICENSE.txt
143
+ - README.md
144
+ - Rakefile
145
+ - bin/console
146
+ - bin/setup
147
+ - lib/backblaze.rb
148
+ - lib/backblaze/b2.rb
149
+ - lib/backblaze/b2/base.rb
150
+ - lib/backblaze/b2/bucket.rb
151
+ - lib/backblaze/b2/file.rb
152
+ - lib/backblaze/b2/file_version.rb
153
+ - lib/backblaze/errors.rb
154
+ - lib/backblaze/utils.rb
155
+ - lib/backblaze/version.rb
156
+ - lib/paperclip/backblaze.rb
157
+ - lib/paperclip/backblaze/version.rb
158
+ - lib/paperclip/storage/backblaze.rb
159
+ - paperclip-backblaze.gemspec
160
+ homepage: https://github.com/alextsui05/paperclip-backblaze
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 2.1.0
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubygems_version: 3.1.6
180
+ signing_key:
181
+ specification_version: 4
182
+ summary: Paperclip storage adapter for Backblaze B2 Cloud.
183
+ test_files: []