📑

RustでGoogleDriveにアップロードする

2024/12/06に公開

目的

ユニークビジョン株式会社 Advent Calendar 2024の12/4の記事です。

Google Driveにファイルをアップロードしたいです。いくつかハマりポイントがありました。

説明

認証

認証にはOAuth認証とサービスアカウント認証があります。どちらでも動きますが、常駐プログラムでずっと動かしたい場合、サービスアカウント認証の方が便利です。

サービスアカウント認証ではGoogleドライブの置きたいフォルダーの共有にサービスアカウントのメールアドレスを設定してコンテンツ管理者に設定してください。これをしないと権限エラーになります。

hyper-rustls

このcrateではdefaultでは他のcrateとコンフリクトしてうまく動きませんでした。
default-features = falseにしてringを追加する必要がありました。

以下の記事を参考にしてください。
Rust rustlsのpanic"no process-level CryptoProvider available -- call CryptoProvider::install_default"

コード

Cargo.toml
[package]
name = "drive2"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
hyper-rustls = { version = "0.27.3", features = ["native-tokio", "http1", "tls12", "logging", "ring"], default-features = false }
google-drive3 = "6.0.0"
serde_json = "1.0.132"
thiserror = "2.0.4"
tokio = { version="1.41.1", features=["macros", "rt-multi-thread", "fs"] }
error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Invalid: {0}")]
    Invalid(String),

    #[error("Json: {0}")]
    Json(#[from] serde_json::Error),

    #[error("IO: {0}")]
    IO(#[from] std::io::Error),

    #[error("GoogleDrive: {0}")]
    GoogleDrive(#[from] google_drive3::Error),
}
google_drive.rs
use std::path::PathBuf;

use google_drive3::{
    api::File,
    hyper_util::{self, client::legacy::connect::HttpConnector},
    yup_oauth2::{self, ServiceAccountKey},
    DriveHub,
};

use crate::error::Error;
use hyper_rustls::HttpsConnector;

pub struct GoogleDrive {
    hub: DriveHub<HttpsConnector<HttpConnector>>,
}

impl GoogleDrive {
    pub async fn new(secret_json: &str) -> Result<Self, Error> {
        let secret: ServiceAccountKey = serde_json::from_str(secret_json)?;
        let auth = yup_oauth2::ServiceAccountAuthenticator::builder(secret)
            .build()
            .await?;

        let client =
            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
                .build(
                    hyper_rustls::HttpsConnectorBuilder::new()
                        .with_native_roots()
                        .unwrap()
                        .https_or_http()
                        .enable_http1()
                        .build(),
                );
        let hub = DriveHub::new(client, auth);
        Ok(Self { hub })
    }

    pub async fn upload_file(
        &self,
        file_name: &str,
        file_path: &PathBuf,
        mime_type: &str,
        folder_id: &str,
    ) -> Result<File, Error> {
        self.upload(
            file_name,
            std::fs::File::open(file_path).unwrap(),
            mime_type,
            folder_id,
        )
        .await
    }

    pub async fn upload_string(
        &self,
        file_name: &str,
        content: &str,
        mime_type: &str,
        folder_id: &str,
    ) -> Result<File, Error> {
        self.upload(
            file_name,
            std::io::Cursor::new(content),
            mime_type,
            folder_id,
        )
        .await
    }

    pub async fn upload<RS>(
        &self,
        file_name: &str,
        content: RS,
        mime_type: &str,
        folder_id: &str,
    ) -> Result<File, Error>
    where
        RS: std::io::Read + Send + std::io::Seek,
    {
        let req = File {
            name: Some(file_name.to_string()),
            mime_type: Some(mime_type.to_string()),
            parents: Some(vec![folder_id.to_string()]),
            ..Default::default()
        };

        let result = self
            .hub
            .files()
            .create(req)
            .supports_all_drives(true)
            .upload(content, mime_type.parse().unwrap())
            .await?;
        Ok(result.1)
    }
}
main.rs
use google_drive::GoogleDrive;

pub mod google_drive;
pub mod error;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let secret = tokio::fs::read_to_string("secret.json").await?;
    let drive = GoogleDrive::new(&secret).await?;
    let folder_id = "xxxxx";
    let res= drive
        .upload_string(
            "test.csv",
            "1,2,3\n4,5,6\n7,8,9",
            "text/csv",
            folder_id,
        )
        .await?;
    println!("{:?}", res);
    Ok(())
}

Discussion