Exporter des fichiers vers Dropbox en Ruby avec le SDK Dropbox
Utilisez le gem communautaire dropbox_api pour
exporter des fichiers locaux depuis Ruby. La commande ci-dessous renouvelle son jeton d’accès,
téléverse les fichiers vers un chemin Dropbox explicite et télécharge le résultat pour vérifier
chaque octet avant de signaler la fin de l’export. Pour une application limitée à son propre dossier,
/report.csv désigne un fichier dans le dossier de votre application ; n’ajoutez pas
/Apps/YourAppFolder au début du chemin.
Prérequis
Ce guide s’adresse aux applications Ruby 3.4 existantes sous Linux, avec Bash et
Bundler installé.
Les versions utilisées pour reproduire l’exemple sont Ruby 3.4.10, Bundler 4.0.23 et
dropbox_api 0.1.21. Ruby 3.4 est
maintenu ; utilisez sa dernière version corrective pour votre
application. Ruby 4.0 est la version majeure stable actuelle, mais ce guide se limite à
l’environnement d’exécution 3.4. La dernière version publiée du gem reste
la version 0.1.21, datant de mars 2022. La compatibilité présentée ici
repose donc sur une combinaison testée, et non sur une promesse de prise en charge des versions
actuelles de Ruby par le projet amont.
Vous avez besoin d’un compte Dropbox et d’un environnement privé côté serveur capable de conserver un secret d’application et un jeton de renouvellement. Utilisez des fichiers ordinaires stables, dont les noms sont en UTF-8. L’exemple accepte les fichiers vides et les données binaires, et limite chaque fichier à 16 MiB, car la vérification charge l’intégralité du téléchargement en mémoire. Les chemins distants doivent contenir uniquement des caractères ASCII ; utilisez le mode fichier pour attribuer un nom distant ASCII à un fichier local dont le nom est en UTF-8. Le mode répertoire exige donc des noms de base ASCII. Exécutez les exports séquentiellement et ne modifiez pas leurs fichiers d’entrée pendant une exécution. La sélection de l’espace de noms d’un espace d’équipe et les écritures concurrentes ne sont pas couvertes par cet exemple.
Configurer une application Dropbox et obtenir des jetons d’accès
Créez une application dans la Dropbox App Console. Choisissez
App folder pour un outil d’export qui n’a besoin d’accéder qu’à ses propres fichiers.
Une application existante de type Full Dropbox
peut utiliser le même code, mais les chemins partent alors de la racine Dropbox par défaut de
l’utilisateur. L’accès limité au dossier de l’application restreint le contenu disponible ;
les portées OAuth limitent séparément les opérations.
Activez files.content.write, files.content.read et
files.metadata.read avant d’accorder l’autorisation : la commande téléverse, télécharge et
vérifie si une destination existe déjà.
Pour une application limitée à son propre dossier, le chemin d’API /Exports/report.csv
se trouve sous le dossier de l’application dans Dropbox. Pour une application disposant d’un accès
complet, il se trouve sous /Exports, dans la racine par défaut de l’utilisateur.
Créez d’abord les éventuels dossiers parents dans Dropbox et respectez exactement leur casse.
Il s’agit de chemins d’API, et non d’URL de sites web ou de chemins
du système de fichiers local. La restriction aux caractères ASCII évite les caractères non ASCII
dans les en-têtes d’arguments avec cette version du gem ; les
règles d’encodage des en-têtes de Dropbox exigent d’échapper ces
caractères.
Fournissez DROPBOX_APP_KEY et DROPBOX_APP_SECRET sous forme de variables
d’environnement, via votre gestionnaire de secrets ou votre environnement shell privé. Ne placez
pas de secrets dans le contrôle de version ou dans les arguments de commande. Nous demanderons une
autorisation d’accès hors ligne une seule fois, puis utiliserons le jeton de renouvellement obtenu
pour les exécutions ultérieures sans intervention. Le
guide du flux d’autorisation par code de Dropbox permet d’omettre
l’URI de redirection pour ce flux d’autorisation local par copier-coller.
Installer le SDK Dropbox pour Ruby
Dans une session Bash sans redéfinition de BUNDLE_*, collez ce bloc depuis un
répertoire parent accessible en écriture. Il crée un nouveau projet dropbox-export ;
si le répertoire existe déjà, une erreur est signalée et il reste intact. Le sous-shell préserve
votre répertoire courant et vos options shell si l’installation échoue.
(
set -eu
command -v ruby >/dev/null
command -v bundle >/dev/null
mkdir dropbox-export
cd dropbox-export
cat > Gemfile <<'RUBY'
source 'https://rubygems.org'
gem 'dropbox_api', '0.1.21'
RUBY
bundle config set --local path vendor/bundle
bundle install
)
Ouvrez le répertoire obtenu dans votre éditeur et utilisez-le comme répertoire de travail pour les
commandes suivantes. Conservez le fichier Gemfile.lock généré avec votre application
pour que les installations ultérieures utilisent les mêmes dépendances résolues. Si l’installation
échoue après la création du projet, examinez ses fichiers et relancez bundle install
depuis ce répertoire ; ne supprimez pas un projet existant pour recommencer la configuration.
S’authentifier auprès de Dropbox en Ruby
Enregistrez ce code dans authorize_dropbox.rb. Sa commande url
affiche une URL d’autorisation. Sa commande exchange lit
DROPBOX_AUTH_CODE dans l’environnement et enregistre uniquement le jeton de
renouvellement dans un nouveau fichier nommé refresh-token, lisible uniquement par
son propriétaire. Elle refuse de remplacer un fichier de jeton existant.
require 'dropbox_api'
def authorize
auth = DropboxApi::Authenticator.new(
ENV.fetch('DROPBOX_APP_KEY'), ENV.fetch('DROPBOX_APP_SECRET')
)
case ARGV
when ['url']
puts auth.auth_code.authorize_url(token_access_type: 'offline')
when ['exchange']
raise 'refresh-token already exists' if File.exist?('refresh-token')
token = auth.auth_code.get_token(ENV.fetch('DROPBOX_AUTH_CODE'))
raise 'No refresh token returned' if token.refresh_token.to_s.empty?
File.open('refresh-token', File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
file.write(token.refresh_token)
end
puts 'Authorization saved to refresh-token'
else
raise ArgumentError, 'Use url or exchange'
end
end
begin
authorize
rescue StandardError => error
warn "Authorization failed (#{error.class})"
exit 1
end
Exécutez :
bundle exec ruby authorize_dropbox.rb url
Ouvrez vous-même l’URL affichée, autorisez l’application et fournissez le code affiché via
DROPBOX_AUTH_CODE dans le même environnement privé. Exécutez ensuite :
bundle exec ruby authorize_dropbox.rb exchange
Gardez refresh-token hors du contrôle de version et restreignez également l’accès
à ses sauvegardes. Vous pouvez le déplacer vers un stockage privé et définir
DROPBOX_REFRESH_TOKEN_FILE sur son chemin absolu. L’outil d’export crée un nouveau jeton d’accès
à partir de ce jeton de renouvellement à chaque invocation ; il ne dépend pas d’un jeton d’accès
généré dans la console qui serait encore valide. Si l’autorisation est révoquée, vous devez répéter
le flux d’autorisation.
Téléverser des fichiers vers Dropbox avec Ruby
Enregistrez ce programme complet dans export_dropbox.rb. Les fichiers de moins de
4 MiB utilisent une seule requête de téléversement ; à partir de 4 MiB, le gem utilise des sessions
de téléversement avec ses blocs de 4 MiB par défaut. Ce seuil volontairement bas permet d’utiliser
le parcours par session tout en respectant la limite de 16 MiB de l’exemple. Le
point de terminaison de téléversement de Dropbox
accepte des requêtes uniques plus volumineuses, mais cet exemple n’a pas besoin de s’approcher de
cette limite.
require 'digest'
require 'dropbox_api'
require 'json'
require 'stringio'
MAX_SINGLE_REQUEST_UPLOAD = 4 * 1024 * 1024
MAX_FILE_BYTES = 16 * 1024 * 1024
def utf8_path(value)
path = value.dup.force_encoding(Encoding::UTF_8)
raise ArgumentError, 'Paths must be valid UTF-8' unless path.valid_encoding?
path
end
def remote_path(value)
path = utf8_path(value)
raise ArgumentError, 'Dropbox paths must contain ASCII only' unless path.ascii_only?
parts = path.split('/', -1)
unless path.start_with?('/') && parts.drop(1).all? { |part| !['', '.', '..'].include?(part) }
raise ArgumentError, 'Use an absolute Dropbox file or folder path without empty, . or .. segments'
end
path
end
def initialize_dropbox_client
auth = DropboxApi::Authenticator.new(
ENV.fetch('DROPBOX_APP_KEY'), ENV.fetch('DROPBOX_APP_SECRET')
)
refresh_token = File.read(ENV.fetch('DROPBOX_REFRESH_TOKEN_FILE', 'refresh-token')).strip
raise ArgumentError, 'Refresh token is empty' if refresh_token.empty?
token = OAuth2::AccessToken.new(auth, '', refresh_token: refresh_token, expires_at: 0)
DropboxApi::Client.new(access_token: token)
end
def with_rate_limit(max_retries = 3)
retries = 0
begin
yield
rescue DropboxApi::Errors::TooManyRequestsError => error
raise if retries >= max_retries
retries += 1
wait = error.retry_after.to_i.positive? ? error.retry_after.to_i : 2**retries
sleep(wait)
retry
end
end
def upload_file(client, file_path, dropbox_path)
local = utf8_path(file_path)
destination = remote_path(dropbox_path)
raise ArgumentError, 'Input must be a regular file' unless File.file?(local)
raise ArgumentError, 'File exceeds 16 MiB' if File.size(local) > MAX_FILE_BYTES
bytes = File.binread(local)
raise ArgumentError, 'File exceeds 16 MiB' if bytes.bytesize > MAX_FILE_BYTES
existing = begin
with_rate_limit { client.get_metadata(destination) }
rescue DropboxApi::Errors::NotFoundError
nil
end
raise 'Destination already exists' unless existing.nil?
uploaded = if bytes.bytesize >= MAX_SINGLE_REQUEST_UPLOAD
with_rate_limit { client.upload_by_chunks(destination, StringIO.new(bytes), mode: :add, autorename: false) }
else
with_rate_limit { client.upload(destination, bytes, mode: :add, autorename: false) }
end
downloaded = nil
stored = with_rate_limit do
client.download("rev:#{uploaded.rev}") do |body|
downloaded = body.nil? ? ''.b : body.b
end
end
unless uploaded.path_display == destination && stored.path_display == destination &&
uploaded.size == bytes.bytesize && stored.size == bytes.bytesize && downloaded == bytes
raise 'Stored file did not match the requested path and bytes'
end
puts JSON.generate(path: destination, bytes: bytes.bytesize, sha256: Digest::SHA256.hexdigest(bytes))
end
class DropboxExporter
def initialize(client)
@client = client
end
def process_directory(local_dir, dropbox_dir)
local = utf8_path(local_dir)
remote = remote_path(dropbox_dir)
raise ArgumentError, 'Input must be a directory' unless File.directory?(local)
Dir.children(local).sort.each do |name|
path = File.join(local, name)
next unless File.file?(path)
upload_file(@client, path, "#{remote}/#{utf8_path(name)}")
end
end
end
if $PROGRAM_NAME == __FILE__
begin
raise ArgumentError, 'Use file|directory LOCAL DROPBOX_PATH' unless ARGV.length == 3
mode, local, remote = ARGV
raise ArgumentError, 'Use file or directory' unless ['file', 'directory'].include?(mode)
client = initialize_dropbox_client
if mode == 'file'
upload_file(client, local, remote)
else
DropboxExporter.new(client).process_directory(local, remote)
end
puts 'Export complete'
rescue StandardError => error
warn "Export failed (#{error.class}); inspect the destination before retrying"
exit 1
end
end
Depuis le répertoire de ce projet, exportez un fichier local existant vers la racine de l’application :
bundle exec ruby export_dropbox.rb file ./report.csv /report.csv
Si la commande réussit, elle affiche un enregistrement JSON contenant le chemin d’API, le nombre d’octets et le hachage SHA-256 ordinaire des octets locaux, suivi de Export complete. Le téléchargement utilise la révision téléversée ; la comparaison vérifie donc l’intégralité du contenu de cette version. Le gem charge ce téléchargement en mémoire ; le téléversement par blocs ne rend pas la consommation mémoire de cet outil d’export constante.
Gérer les erreurs et les exceptions
La commande se termine avec un code de sortie non nul en cas de fichiers ou de configuration
manquants, d’autorisation refusée, de destinations existantes, d’échec de finalisation d’une
session ou de différences lors de la vérification par relecture. Son diagnostic indique la classe
de l’exception sans afficher le corps des réponses du fournisseur ni les informations d’accès.
Une exception NotFoundError pendant la relecture signifie que la révision demandée
n’a pas pu être lue ; vérifiez la destination et les autorisations de l’application. Une exception
OAuth2::Error pendant le renouvellement signifie que l’autorisation n’a pas abouti.
Seule l’exception TooManyRequestsError déclenche de nouvelles tentatives, jusqu’à trois fois
par opération. Le délai utilise la valeur de retry_after si elle est positive ;
sinon, les délais sont de 2, 4 et 8 secondes. Les autres erreurs se propagent. Si un téléversement
réussit, mais que sa réponse ou le résultat de sa vérification est perdu, le fichier distant peut
tout de même exister : examinez-le avant de réessayer. Le script ne supprime pas les données
distantes en cas d’échec. Une session démarrée peut aussi rester inachevée.
La vérification d’une destination existante fait échouer les réexécutions séquentielles, même si les
octets sont identiques. Ce n’est pas un verrou : empêchez toute autre écriture dans le dossier de
destination pendant un export. Les téléversements demandent mode: :add et
autorename: false ; le script ne demande jamais d’écrasement et n’accepte jamais un
résultat renommé comme un succès.
Automatiser les exports de fichiers dans votre application Ruby
Créez /Exports dans le dossier de votre application dans Dropbox, puis exportez
les fichiers ordinaires situés directement dans un répertoire local exports
existant :
bundle exec ruby export_dropbox.rb directory ./exports /Exports
Le mode répertoire inclut les fichiers ordinaires cachés, ignore les sous-répertoires et s’arrête au premier échec. Les fichiers précédents restent exportés et vérifiés ; il ne s’agit pas d’une transaction de répertoire de type tout ou rien. Un répertoire vide ne contient aucun fichier à exporter. Utilisez un nouveau dossier de destination pour chaque lot planifié, ou conservez la liste des chemins déjà traités avant de sélectionner les fichiers d’entrée d’une autre exécution.
Dans une tâche Rails, chargez export_dropbox.rb, créez le client à l’intérieur de la
tâche et appelez upload_file ou DropboxExporter.new(client).process_directory. Ces méthodes lèvent
une exception en cas d’échec ; laissez donc la tâche consigner cet échec. Gardez ses informations
d’accès disponibles via la même configuration privée et examinez tout résultat distant incertain
avant que la tâche ne fasse une nouvelle tentative.
