├── Rakefile ├── .gitignore ├── lib ├── paperclipdropbox │ ├── version.rb │ └── railtie.rb ├── tasks │ └── paperclipdropbox.rake └── paperclipdropbox.rb ├── Gemfile ├── paperclipdropbox.gemspec └── README.rdoc /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .rvmrc 6 | -------------------------------------------------------------------------------- /lib/paperclipdropbox/version.rb: -------------------------------------------------------------------------------- 1 | module Paperclipdropbox 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in paperclipdropbox.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/paperclipdropbox/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'paperclipdropbox' 2 | require 'rails' 3 | 4 | module Paperclipdropbox 5 | class Railtie < Rails::Railtie 6 | 7 | rake_tasks do 8 | load "tasks/paperclipdropbox.rake" 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /paperclipdropbox.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "paperclipdropbox/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "paperclipdropbox" 7 | s.version = Paperclipdropbox::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Paul Ketelle"] 10 | s.email = ["paul@ketelle.com"] 11 | s.homepage = "https://github.com/dripster82/paperclipdropbox" 12 | s.summary = %q{Dropbox storage support for paperclip file attachment} 13 | s.description = %q{Adds Dropbox storage support for the Paperclip gem. Dropbox account required.} 14 | 15 | s.rubyforge_project = "paperclipdropbox" 16 | 17 | s.add_dependency 'kt-paperclip' 18 | s.add_dependency 'dropbox_api' 19 | s.add_dependency 'http' 20 | 21 | s.files = `git ls-files`.split("\n") 22 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 24 | s.require_paths = ["lib"] 25 | end 26 | -------------------------------------------------------------------------------- /lib/tasks/paperclipdropbox.rake: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "dropbox_api" 3 | require "paperclip" 4 | require "paperclipdropbox" 5 | 6 | namespace :paperclipdropbox do 7 | 8 | desc "Authorize Paperclip link to your Dropbox" 9 | task authorize: :environment do 10 | 11 | config_file = Paperclip::Storage::Dropbox::CONFIG_FILE 12 | puts "" 13 | puts "" 14 | puts "" 15 | 16 | begin 17 | dropbox_key = '8ti7qntpcysl91j' 18 | dropbox_secret = 'i0tshr4cpd1pa4e' 19 | 20 | authenticator = DropboxApi::Authenticator.new(dropbox_key, dropbox_secret) 21 | auth_url = authenticator.auth_code.authorize_url(token_access_type: 'offline') 22 | 23 | puts "" 24 | puts "Please go to #{auth_url} and approve the app" 25 | puts "" 26 | puts "Please enter you access code" 27 | access_code = gets.chomp 28 | 29 | access_token = authenticator.auth_code.get_token(access_code) 30 | 31 | if File.exists?("#{Rails.root}#{config_file}") 32 | config = YAML.load_file("#{Rails.root}#{config_file}") 33 | else 34 | config = {} 35 | end 36 | config[:dropbox_key] = dropbox_key 37 | config[:dropbox_secret] = dropbox_secret 38 | config[:access_token] = access_token.to_hash 39 | 40 | File.open("#{Rails.root}#{config_file}",'w') do |h| 41 | h.write config.to_yaml 42 | end 43 | 44 | puts "" 45 | puts "Paperclip is now Authorized" 46 | 47 | rescue => error 48 | p error 49 | puts "Failed Authorization. Please try again." 50 | end 51 | 52 | puts "" 53 | puts "" 54 | end 55 | 56 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = PaperclipDropboxStorage 2 | 3 | Dropbox storage support for kt-paperclip file attachment plugin. 4 | 5 | == Install 6 | 7 | You can let bundler install Paperclip Dropbox Plugin by adding this line to your application's Gemfile: 8 | 9 | gem 'paperclipdropbox' 10 | 11 | And then execute: 12 | 13 | bundle install 14 | 15 | Or install it yourself as: 16 | 17 | gem install paperclipdropbox 18 | 19 | Then run the authotization task 20 | 21 | rails paperclipdropbox:authorize 22 | 23 | you'll then be given a url to login to dropbox to authorize this plugin access to your dropbox account. 24 | 25 | 26 | == Usage 27 | 28 | In your model: 29 | 30 | class User < ActiveRecord::Base 31 | has_attached_file :avatar, 32 | styles: { medium: "300x300>", thumb: "100x100>" }, 33 | default_url: "/missing.png", 34 | storage: :Dropbox, 35 | path: ":class/:attachment/:style/:id_:filename" 36 | end 37 | 38 | Add a migration script to add the dropbox_share_urls column 39 | 40 | class AddDropboxShareIdsToUsers < ActiveRecord::Migration[7.0] 41 | def change 42 | add_column :users, :dropbox_share_urls, :string, default: "{}", null: false 43 | end 44 | end 45 | 46 | == Optional 47 | 48 | === App Folder 49 | This is the first folder in your dropbox before the paths take effect 50 | 51 | ==== Usage 52 | app_folder: "Rails_Super_App", # defaults to "Paperclip_Dropbox" if not passed 53 | 54 | === Remove Url Redirects 55 | This is a experimental.. the urls provided by Dropbox are redirecting urls. this adds additional time to the image load to the user. 56 | this option will follow the redirect url and get the direct url to pass to the browser. 57 | 58 | However this url will expire in 3-4 hours. The direct url is cached in Rails with a lifetime of 3 hours. at wich point it will retreive a new direct url to use. 59 | 60 | AGAIN EXPERIMENTAL so this can cause longer page loads when it needs to go and get the new direct url. 61 | 62 | ==== Usage 63 | remove_url_redirects: true, # default is false 64 | -------------------------------------------------------------------------------- /lib/paperclipdropbox.rb: -------------------------------------------------------------------------------- 1 | module Paperclipdropbox 2 | require 'paperclipdropbox/railtie' if defined?(Rails) 3 | end 4 | 5 | require 'http' 6 | require "dropbox_api" 7 | 8 | module Paperclip 9 | module Storage 10 | module Dropbox 11 | 12 | CONFIG_FILE = "/config/paperclipdropbox.yml" 13 | 14 | def self.extended(base) 15 | base.instance_eval do 16 | @options[:escape_url] = false 17 | app_folder = @options[:app_folder] || 'Paperclip_Dropbox/' 18 | unless @options[:url].to_s.match(/\A:dropdox.com\z/) 19 | @options[:path] = app_folder + @options[:path].gsub(/:url/, @options[:url]).gsub(/\A:rails_root\/public\/system\//, "") 20 | @options[:url] = ":dropbox_file_url" 21 | end 22 | end 23 | 24 | unless Paperclip::Interpolations.respond_to? :dropbox_file_url 25 | Paperclip.interpolates(:dropbox_file_url) do |attachment, style| 26 | attachment.public_url(style) 27 | end 28 | end 29 | end 30 | 31 | def exists?(style = default_style) 32 | begin 33 | dropbox_client.get_metadata("/#{path(style)}") 34 | true 35 | rescue 36 | false 37 | end 38 | end 39 | 40 | def flush_writes 41 | share_urls = false 42 | share_urls = {} unless @queued_for_write.empty? 43 | @queued_for_write.each do |style, file| 44 | begin 45 | dropbox_client.upload_by_chunks "/#{path(style)}", file 46 | public_url(style, file.size) 47 | rescue 48 | end 49 | end 50 | 51 | after_flush_writes 52 | @queued_for_write = {} 53 | end 54 | 55 | def flush_deletes 56 | path_styles = styles.keys.push(:original).map {|val| val.to_s} 57 | 58 | @queued_for_delete.each do |path| 59 | begin 60 | 61 | dropbox_client.delete("/#{path}") 62 | rescue 63 | end 64 | end 65 | 66 | if has_dropbox_share_urls? && !@queued_for_delete.empty? 67 | new_share_urls = dropbox_share_urls 68 | 69 | path_styles.each do |style| 70 | Rails.cache.delete("#{@instance.class}_#{@instance.id}_#{name}_#{style}") 71 | new_share_urls.delete("#{name}_#{style}") 72 | end 73 | 74 | update_dropbox_share_urls(new_share_urls.to_json)if @instance.persisted? 75 | end 76 | 77 | @queued_for_delete = [] 78 | end 79 | 80 | def public_url(style = default_style, size = 0) 81 | return cached_url(style, size) if can_use_cached_url?("#{name}_#{style}") 82 | 83 | shared_link = dropbox_shared_link(style, size) 84 | 85 | if has_dropbox_share_urls? 86 | new_share_urls = dropbox_share_urls 87 | 88 | new_share_urls["#{name}_#{style}"] = shared_link 89 | update_dropbox_share_urls(new_share_urls.to_json) 90 | end 91 | shared_link 92 | end 93 | 94 | private 95 | 96 | def dropbox_shared_link(style = default_style, size = 0) 97 | begin 98 | shared_link = @options[:default_url] 99 | shared_link = dropbox_client.list_shared_links(path: "/#{path(style)}").links.first.url.gsub("/s/", "/s/raw/") 100 | rescue 101 | shared_link = dropbox_client.create_shared_link_with_settings("/#{path(style)}").url.gsub("/s/", "/s/raw/") 102 | end 103 | 104 | shared_link 105 | end 106 | 107 | def remove_redirects(shared_link) 108 | loop do 109 | res = HTTP.get(shared_link) 110 | 111 | break unless res.status == 302 112 | shared_link = res['location'] 113 | end 114 | 115 | shared_link 116 | end 117 | 118 | def dropbox_share_urls 119 | begin 120 | @_dropbox_share_urls ||= JSON.parse(@instance.dropbox_share_urls) 121 | rescue 122 | @_dropbox_share_urls ||= {} 123 | end 124 | end 125 | 126 | def update_dropbox_share_urls(value) 127 | @instance.update_column(:dropbox_share_urls, value) 128 | end 129 | 130 | def has_dropbox_share_urls? 131 | @instance.has_attribute?(:dropbox_share_urls) 132 | end 133 | 134 | def max_size 135 | @_max_size ||= @options['max_file_size'] || 1024 * 1024 * 3 136 | end 137 | 138 | def cached_url(style = default_style, size = 0) 139 | if has_dropbox_share_urls? 140 | remove_url_redirects = @options[:remove_url_redirects] || false 141 | 142 | if dropbox_share_urls.has_key?("#{name}_#{style}") 143 | return dropbox_share_urls["#{name}_#{style}"] unless remove_url_redirects 144 | 145 | return Rails.cache.fetch("#{@instance.class}_#{@instance.id}_#{name}_#{style}", expires_in: 3.hours) do 146 | shared_link = dropbox_share_urls["#{name}_#{style}"] 147 | shared_link = remove_redirects(shared_link) if remove_url_redirects && size <= max_size 148 | 149 | shared_link 150 | end 151 | end 152 | end 153 | 154 | false 155 | end 156 | 157 | def can_use_cached_url?(style_key) 158 | has_dropbox_share_urls? && dropbox_share_urls.has_key?(style_key) 159 | end 160 | 161 | def dropbox_client 162 | if @_dropbox_client.blank? 163 | if File.exists?("#{Rails.root}#{CONFIG_FILE}") 164 | dropbox_config = YAML.load_file("#{Rails.root}#{CONFIG_FILE}") 165 | authenticator = DropboxApi::Authenticator.new(dropbox_config[:dropbox_key], dropbox_config[:dropbox_secret]) 166 | access_token = OAuth2::AccessToken.from_hash(authenticator, dropbox_config[:access_token]) 167 | @_dropbox_client = DropboxApi::Client.new( 168 | access_token: access_token, 169 | on_token_refreshed: lambda { |new_token_hash| 170 | dropbox_config = YAML.load_file("#{Rails.root}#{CONFIG_FILE}") 171 | dropbox_config[:access_token] = new_token_hash 172 | File.open("#{Rails.root}#{CONFIG_FILE}",'w') do |h| 173 | h.write dropbox_config.to_yaml 174 | end 175 | } 176 | ) 177 | else 178 | warn("#{CONFIG_FILE} does not exist\nEnsure you have authorised paperclipdropbox") 179 | end 180 | end 181 | 182 | @_dropbox_client 183 | end 184 | end 185 | end 186 | end 187 | --------------------------------------------------------------------------------