lib/backup/storage/dropbox.rb
require "dropbox_sdk"
module Backup
module Storage
class Dropbox < Base
include Storage::Cycler
class Error < Backup::Error; end
##
# Dropbox API credentials
attr_accessor :api_key, :api_secret
##
# Path to store cached authorized session.
#
# Relative paths will be expanded using Config.root_path,
# which by default is ~/Backup unless --root-path was used
# on the command line or set in config.rb.
#
# By default, +cache_path+ is '.cache', which would be
# '~/Backup/.cache/' if using the default root_path.
attr_accessor :cache_path
##
# Dropbox Access Type
# Valid values are:
# :app_folder (default)
# :dropbox (full access)
attr_accessor :access_type
##
# Chunk size, specified in MiB, for the ChunkedUploader.
attr_accessor :chunk_size
##
# Number of times to retry failed operations.
#
# Default: 10
attr_accessor :max_retries
##
# Time in seconds to pause before each retry.
#
# Default: 30
attr_accessor :retry_waitsec
##
# Creates a new instance of the storage object
def initialize(model, storage_id = nil)
super
@path ||= "backups"
@cache_path ||= ".cache"
@access_type ||= :app_folder
@chunk_size ||= 4 # MiB
@max_retries ||= 10
@retry_waitsec ||= 30
path.sub!(/^\//, "")
end
private
##
# The initial connection to Dropbox will provide the user with an
# authorization url. The user must open this URL and confirm that the
# authorization successfully took place. If this is the case, then the
# user hits 'enter' and the session will be properly established.
# Immediately after establishing the session, the session will be
# serialized and written to a cache file in +cache_path+.
# The cached file will be used from that point on to re-establish a
# connection with Dropbox at a later time. This allows the user to avoid
# having to go to a new Dropbox URL to authorize over and over again.
def connection
return @connection if @connection
unless session = cached_session
Logger.info "Creating a new session!"
session = create_write_and_return_new_session!
end
# will raise an error if session not authorized
@connection = DropboxClient.new(session, access_type)
rescue => err
raise Error.wrap(err, "Authorization Failed")
end
##
# Attempt to load a cached session
def cached_session
session = false
if File.exist?(cached_file)
begin
session = DropboxSession.deserialize(File.read(cached_file))
Logger.info "Session data loaded from cache!"
rescue => err
Logger.warn Error.wrap(err, <<-EOS)
Could not read session data from cache.
Cache data might be corrupt.
EOS
end
end
session
end
##
# Transfer each of the package files to Dropbox in chunks of +chunk_size+.
# Each chunk will be retried +chunk_retries+ times, pausing +retry_waitsec+
# between retries, if errors occur.
def transfer!
package.filenames.each do |filename|
src = File.join(Config.tmp_path, filename)
dest = File.join(remote_path, filename)
Logger.info "Storing '#{dest}'..."
uploader = nil
File.open(src, "r") do |file|
uploader = connection.get_chunked_uploader(file, file.stat.size)
while uploader.offset < uploader.total_size
with_retries do
uploader.upload(1024**2 * chunk_size)
end
end
end
with_retries do
uploader.finish(dest)
end
end
rescue => err
raise Error.wrap(err, "Upload Failed!")
end
def with_retries
retries = 0
begin
yield
rescue StandardError => err
retries += 1
raise if retries > max_retries
Logger.info Error.wrap(err, "Retry ##{retries} of #{max_retries}.")
sleep(retry_waitsec)
retry
end
end
# Called by the Cycler.
# Any error raised will be logged as a warning.
def remove!(package)
Logger.info "Removing backup package dated #{package.time}..."
connection.file_delete(remote_path_for(package))
end
def cached_file
path = cache_path.start_with?("/") ?
cache_path : File.join(Config.root_path, cache_path)
File.join(path, api_key + api_secret)
end
##
# Serializes and writes the Dropbox session to a cache file
def write_cache!(session)
FileUtils.mkdir_p File.dirname(cached_file)
File.open(cached_file, "w") do |cache_file|
cache_file.write(session.serialize)
end
end
##
# Create a new session, write a serialized version of it to the
# .cache directory, and return the session object
def create_write_and_return_new_session!
require "timeout"
session = DropboxSession.new(api_key, api_secret)
# grab the request token for session
session.get_request_token
template = Backup::Template.new(
session: session, cached_file: cached_file
)
template.render("storage/dropbox/authorization_url.erb")
# wait for user to hit 'return' to continue
Timeout.timeout(180) { STDIN.gets }
# this will raise an error if the user did not
# visit the authorization_url and grant access
#
# get the access token from the server
# this will be stored with the session in the cache file
session.get_access_token
template.render("storage/dropbox/authorized.erb")
write_cache!(session)
template.render("storage/dropbox/cache_file_written.erb")
session
rescue => err
raise Error.wrap(err, "Could not create or authenticate a new session")
end
end
end
end