Dateien mit dem Dropbox SDK in Ruby zu Dropbox exportieren
Nutzen Sie das Community-Gem dropbox_api,
um lokale Dateien aus Ruby zu exportieren. Der folgende Befehl erneuert sein Zugriffstoken,
lädt Dateien in einen expliziten Dropbox-Pfad hoch und lädt das Ergebnis herunter, um vor der
Abschlussmeldung jedes Byte zu prüfen. Bei einer App mit App-Ordner-Zugriff bezeichnet
/report.csv eine Datei im Ordner Ihrer App; stellen Sie
/Apps/YourAppFolder nicht voran.
Voraussetzungen
Diese Anleitung richtet sich an bestehende Ruby-3.4-Anwendungen unter Linux mit Bash und
installiertem Bundler.
Die Versionen zur Reproduktion sind Ruby 3.4.10, Bundler 4.0.23 und
dropbox_api 0.1.21. Ruby 3.4 wird
gepflegt; verwenden Sie für Ihre Anwendung die aktuelle
Version dieser Reihe mit allen Patches. Ruby 4.0 ist die aktuelle stabile Hauptversion, doch diese
Anleitung beschränkt sich auf Ruby 3.4 als Laufzeitversion. Die neueste veröffentlichte Version des Gems
ist weiterhin 0.1.21 vom März 2022. Die Kompatibilität beruht hier
also auf einer getesteten Kombination, nicht auf einer Zusage des Projekts, aktuelle Ruby-Versionen
zu unterstützen.
Sie benötigen ein Dropbox-Konto und eine private serverseitige Umgebung, die ein App-Secret und ein Refresh-Token sicher aufbewahren kann. Verwenden Sie stabile reguläre Dateien mit UTF-8-Dateinamen. Das Beispiel akzeptiert leere Dateien und Binärdaten und begrenzt jede Datei auf 16 MiB, da die Überprüfung den vollständigen Download zwischenspeichert. Entfernte Pfade dürfen nur ASCII-Zeichen enthalten. Verwenden Sie den Dateimodus, um einer lokalen Datei mit UTF-8-Dateinamen einen ASCII-Namen am Ziel zu geben. Der Verzeichnismodus erfordert folglich ASCII-Basisnamen. Führen Sie Exporte nacheinander aus und ändern Sie während eines Durchlaufs keine Eingabedateien. Die Auswahl des Team-Space-Namensraums und gleichzeitige Schreibzugriffe sind nicht Teil dieses Beispiels.
Dropbox-App einrichten und Zugriffstoken abrufen
Erstellen Sie eine App in der Dropbox App Console. Wählen Sie
App folder für ein Exportprogramm, das nur seine eigenen Dateien benötigt.
Eine bestehende App mit Full Dropbox kann denselben Code verwenden.
Die Pfade beginnen dann jedoch im standardmäßigen Dropbox-Stammordner des Benutzers.
App-Ordner-Zugriff begrenzt die verfügbaren Inhalte;
OAuth-Scopes begrenzen unabhängig davon die Vorgänge.
Aktivieren Sie vor der Autorisierung files.content.write,
files.content.read und files.metadata.read: Der Befehl lädt Dateien hoch
und herunter und prüft, ob das Ziel bereits existiert.
Bei einer App mit App-Ordner-Zugriff liegt der API-Pfad /Exports/report.csv in Dropbox
unterhalb des App-Ordners. Bei einer App mit Vollzugriff liegt er unterhalb von
/Exports im standardmäßigen Stammordner des Benutzers. Erstellen Sie
übergeordnete Ordner vorab in Dropbox und beachten Sie deren genaue Groß- und Kleinschreibung.
Dies sind API-Pfade, keine Website-URLs oder lokalen
Dateisystempfade. Die ASCII-Beschränkung vermeidet Nicht-ASCII-Zeichen in Argument-Headern dieser
Gem-Version. Die Regeln zur Header-Kodierung von Dropbox
verlangen, dass solche Zeichen maskiert werden.
Stellen Sie DROPBOX_APP_KEY und DROPBOX_APP_SECRET über Ihren
Secret-Manager oder eine private Shell-Umgebung als Umgebungsvariablen bereit. Speichern Sie keine
sensiblen Zugangsdaten in der Versionsverwaltung und übergeben Sie sie nicht als Befehlsargumente.
Wir fordern einmalig eine Offline-Autorisierung an und nutzen deren Refresh-Token für spätere
unbeaufsichtigte Durchläufe. Die Code-Flow-Anleitung von Dropbox
erlaubt es, bei diesem lokalen Autorisierungsablauf per Kopieren und Einfügen auf eine
Redirect-URI zu verzichten.
Dropbox SDK für Ruby installieren
Fügen Sie diesen Block in einer Bash-Sitzung ohne Überschreibungen durch
BUNDLE_* ein, während Sie sich in einem beschreibbaren übergeordneten
Verzeichnis befinden. Er erstellt ein neues Projekt namens dropbox-export.
Ein bereits vorhandenes Verzeichnis führt zu einem Fehler und bleibt unverändert.
Falls die Installation fehlschlägt, bewahrt die Subshell Ihr aktuelles Verzeichnis und Ihre
Shell-Optionen.
(
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
)
Öffnen Sie das erstellte Verzeichnis in Ihrem Editor und verwenden Sie es für die übrigen Befehle
als Arbeitsverzeichnis. Bewahren Sie die erzeugte Datei Gemfile.lock zusammen
mit Ihrer Anwendung auf, damit spätere Installationen dieselben aufgelösten Abhängigkeiten nutzen.
Falls die Installation nach dem Erstellen des Projekts fehlschlägt, prüfen Sie dessen Dateien und
führen Sie bundle install in diesem Verzeichnis erneut aus. Löschen Sie kein
bestehendes Projekt, um die Einrichtung neu zu starten.
In Ruby bei Dropbox authentifizieren
Speichern Sie dies als authorize_dropbox.rb. Der Befehl
url gibt eine Autorisierungs-URL aus. Der Befehl
exchange liest DROPBOX_AUTH_CODE aus der Umgebung und
speichert nur das Refresh-Token in einer neuen, nur vom Eigentümer lesbaren Datei namens
refresh-token. Eine bestehende Tokendatei wird nicht ersetzt.
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
Führen Sie Folgendes aus:
bundle exec ruby authorize_dropbox.rb url
Öffnen Sie die ausgegebene URL selbst, autorisieren Sie die App und stellen Sie den angezeigten Code
über DROPBOX_AUTH_CODE in derselben privaten Umgebung bereit. Führen Sie dann
Folgendes aus:
bundle exec ruby authorize_dropbox.rb exchange
Halten Sie refresh-token aus der Versionsverwaltung heraus und beschränken Sie
auch den Zugriff auf Sicherungskopien. Sie können die Datei in einen privaten Speicher verschieben
und DROPBOX_REFRESH_TOKEN_FILE auf ihren absoluten Pfad setzen. Das Exportprogramm erzeugt
bei jedem Aufruf aus diesem Refresh-Token ein neues Zugriffstoken. Es ist nicht darauf angewiesen,
dass ein in der Konsole erzeugtes Zugriffstoken noch gültig ist. Wird die Autorisierung widerrufen,
muss der Autorisierungsablauf wiederholt werden.
Dateien mit Ruby zu Dropbox hochladen
Speichern Sie dieses vollständige Programm als export_dropbox.rb. Dateien unter
4 MiB werden mit einer einzigen Upload-Anfrage übertragen. Ab 4 MiB verwendet das Gem
Upload-Sitzungen mit seiner Standard-Blockgröße von 4 MiB. Dieser bewusst niedrige Schwellenwert
hält den Sitzungsablauf innerhalb der Grenze von 16 MiB des Beispiels nutzbar. Der
Upload-Endpunkt von Dropbox
erlaubt größere Einzelanfragen, doch dieses Beispiel muss sich dieser Grenze nicht nähern.
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
Exportieren Sie aus diesem Projektverzeichnis eine vorhandene lokale Datei in den App-Stammordner:
bundle exec ruby export_dropbox.rb file ./report.csv /report.csv
Ein erfolgreicher Befehl gibt einen JSON-Datensatz mit dem API-Pfad, der Byte-Anzahl und dem gewöhnlichen SHA-256-Hash der lokalen Bytes aus, gefolgt von Export complete. Der Download verwendet die hochgeladene Revision, sodass der Vergleich den vollständigen Inhalt dieser Version prüft. Das Gem speichert diesen Download zwischen. Die Aufteilung des Uploads in Blöcke sorgt also nicht für einen konstanten Speicherbedarf des Exportprogramms.
Fehler und Ausnahmen behandeln
Der Befehl endet mit einem Exitcode ungleich null, wenn Dateien oder Konfiguration fehlen, die
Autorisierung verweigert wird, Ziele bereits existieren, der Sitzungsabschluss fehlschlägt oder
zurückgelesene Daten abweichen. Seine Diagnose nennt die Ausnahmeklasse, ohne Antwortkörper des
Anbieters oder Zugangsdaten auszugeben. Eine Ausnahme vom Typ NotFoundError
beim Zurücklesen bedeutet, dass die angeforderte Revision nicht gelesen werden konnte. Prüfen Sie
das Ziel und die App-Berechtigungen. Eine Ausnahme vom Typ OAuth2::Error
beim Erneuern bedeutet, dass die Autorisierung nicht erfolgreich war.
Nur bei TooManyRequestsError wird ein Vorgang erneut versucht, bis zu dreimal pro
Vorgang. Die Wartezeit richtet sich nach einem positiven Wert von
retry_after, andernfalls beträgt sie 2, 4 und 8 Sekunden. Andere Fehler
werden weitergegeben. Wenn ein Upload erfolgreich ist, aber seine Antwort oder Überprüfung
verloren geht, kann die entfernte Datei dennoch existieren: Prüfen Sie sie vor einem erneuten
Versuch. Das Skript löscht bei Fehlern keine entfernten Daten. Auch eine gestartete Sitzung kann
unvollendet bleiben.
Die Prüfung auf ein vorhandenes Ziel lässt aufeinanderfolgende Wiederholungen selbst bei
identischen Bytes fehlschlagen. Sie ist keine Sperre: Verhindern Sie während eines Exports andere
Schreibzugriffe auf den Zielordner. Uploads fordern mode: :add und
autorename: false an. Das Skript fordert niemals ein Überschreiben an und
akzeptiert kein umbenanntes Ergebnis als Erfolg.
Dateiexporte in Ihrer Ruby-Anwendung automatisieren
Erstellen Sie /Exports in Ihrem App-Ordner in Dropbox. Exportieren Sie dann
die regulären Dateien, die direkt in einem vorhandenen lokalen Verzeichnis namens
exports liegen:
bundle exec ruby export_dropbox.rb directory ./exports /Exports
Der Verzeichnismodus schließt versteckte reguläre Dateien ein, überspringt Unterverzeichnisse und bricht beim ersten Fehler ab. Frühere Dateien bleiben exportiert und überprüft. Dies ist keine Verzeichnistransaktion nach dem Alles-oder-nichts-Prinzip. Ein leeres Verzeichnis enthält keine Dateien zum Exportieren. Verwenden Sie für jeden geplanten Stapellauf einen neuen Zielordner oder erfassen Sie bereits abgeschlossene Pfade, bevor Sie Eingaben für einen weiteren Durchlauf wählen.
Binden Sie in einem Rails-Job export_dropbox.rb ein, erstellen Sie den Client innerhalb
des Jobs und rufen Sie upload_file oder DropboxExporter.new(client).process_directory auf.
Diese Methoden lösen bei Fehlern Ausnahmen aus; lassen Sie den Job diesen Fehler daher erfassen.
Stellen Sie seine Zugangsdaten über dieselbe private Konfiguration bereit und prüfen Sie jedes
ungewisse entfernte Ergebnis, bevor der Job einen erneuten Versuch unternimmt.
