0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unityエディタでスプレッドシートをエクセルとしてダウンロード

Posted at

2022/12/16 : 初稿
Unity : 2021.3.15f1

作ってるゲームのマスターデータをスプレッドシートで管理したいけれど、
スプレッドシートをGAS使って解析するのはしんどいし
2分以内に終わらないと打ち切られてしまう。

ならば、エクセルとしてダウンロードしてから、
ローカルでNPOIとか使って解析すればいいじゃん的な。

やりたいこと

https://drive.google.com/drive/folders/うんたらかんたら
にあるスプレッドシートを全て
Unityのプロジェクトフォルダ直下にあるExcelsフォルダに
ダウンロードしたいとします。

スプレッドシートをエクセルとしてダウンロードするUnity側のスクリプト

こいつをAssets/Editorとかに置きます。
また、Unityのプロジェクト直下にExcelsフォルダを掘ります。

Assets/Editor/MasterDownloader.cs
///
/// @file   MasterDownloader.cs
/// @author KYukimoto
/// @date   
///
/// @brief  スプレッドシートをエクセルとしてダウンロード
/// 
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Google.Apis.Download;
using Google.Apis.Drive.v3;
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

namespace Utils
{
    // サンプル
    class SampleMasterDownloader : MasterDownloader
    {
        // GoogleAPI : Google Developer Consoleで作成したプロジェクト名
        protected override string GoogleApplicationName { get { return "ExcelDownloader"; } }

        // マスター置き場のフォルダId : GoogleDriveをブラウザで開いた時のURL https://drive.google.com/drive/folders/うんたらかんたら のウンタラカンタラ部分
        protected override string FolderId { get { return "XXXXXXXXXXXXXXXXXXXXXXXXXX"; } }

        // ダウンロードエントリポイント
        [MenuItem("Utils/マスターデータ/スプレッドシート/エクセルとしてダウンロード")]
        static void DownloadExcels()
        {
            SampleMasterDownloader instance = new SampleMasterDownloader();
            instance.Download();
        }

        // アップロードエントリポイント
        [MenuItem("Utils/マスターデータ/スプレッドシート/エクセルをアップロード")]
        static void UploadExcels()
        {
            SampleMasterDownloader instance = new SampleMasterDownloader();
            instance.Upload();
        }
    }

    // MasterDownloader
    abstract class MasterDownloader
    {
        // GoogleAPI : Google Developer Consoleで作成したプロジェクト名
        protected abstract string GoogleApplicationName { get; }

        // マスター置き場のフォルダId : GoogleDriveをブラウザで開いた時のURL https://drive.google.com/drive/folders/うんたらかんたら のウンタラカンタラ部分
        protected abstract string FolderId { get; }
            
        // エクセル置き場
        protected virtual string ExcelsPath { get { return "Excels"; } }
        protected virtual string TimeStampsPath { get { return "SpreadSheets"; } }

        // client_secret.jsonの置き場所
        protected virtual string ClientSecretJsonPath
        {
            get
            {
                // デフォルトはこのソースファイル(MasterDownloader.cs)と同じフォルダ
                var stackFrame = new System.Diagnostics.StackFrame(true);
                var thisFilePath = stackFrame.GetFileName();
                var thisDir = Path.GetDirectoryName(thisFilePath);
                var client_secret_path = Path.Combine(thisDir, "client_secret.json");
                return client_secret_path;
            }
        }

        // 認証結果保存場所
        protected virtual string StoreCredentialsPath
        {
            get
            {
                // デフォルトはこのソースファイル(MasterDownloader.cs)と同じフォルダ
                var stackFrame = new System.Diagnostics.StackFrame(true);
                var thisFilePath = stackFrame.GetFileName();
                var thisDir = Path.GetDirectoryName(thisFilePath);
                var credPath = Path.Combine(thisDir, "credentials");
                return credPath;
            }
        }

        // スプレッドシートをエクセルとしてダウンロード
        (string desiredPath, string savedPath) DownloadExcel(DriveService service, string excelsPath, string googleDriveFileID, string fileName)
        {
            // ダウンロードファイル名
            var desiredPath = excelsPath + "/" + fileName + ".xlsx";
            var savedPath = excelsPath + "/__" + fileName + ".xlsx";

            // スプレッドシートをエクセルに変換
            var fileId = googleDriveFileID;
            var request = service.Files.Export(fileId, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

            // ダウンロード
            var stream = new MemoryStream();
            request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) => {
                switch (progress.Status) {
                case DownloadStatus.Downloading:
                    Debug.Log(progress.BytesDownloaded + "byteダウンロードしました...");
                    break;
                case DownloadStatus.Completed:
                    File.Delete(savedPath);
                    var fileStream = new FileStream(savedPath, FileMode.Create);
                    stream.WriteTo(fileStream);
                    fileStream.Close();
                    Debug.Log("スプレッドシートのダウンロード完了.");
                    break;
                case DownloadStatus.Failed:
                    Debug.Log("スプレッドシートのダウンロードに失敗しました.");
                    break;
                }
            };
            var result = request.DownloadWithStatus(stream);

            // 成否を返す
            return (result.Status == DownloadStatus.Completed) ? (desiredPath, savedPath) : (null, null);
        }

        // ダウンロード処理エントリポイント
        protected void Download()
        {
            // 確認
            var ret = EditorUtility.DisplayDialogComplex("確認", "GoogleDriveからスプレッドシートをダウンロードします", "差分のみ", "キャンセル", "フル");
            if (ret == 1) {
                return;
            }
            var full = ret == 2;

            // 処理中...
            EditorUtility.DisplayProgressBar("処理中", "GoogleAPIを設定しています...", 0);

            // 処理
            var downloadedPaths = new Dictionary<string, string>();
            try {
                DownloadCore(downloadedPaths, full);
            } catch (Exception e) {
                Debug.LogError("エラー:" + e.ToString());
                EditorUtility.DisplayDialog("失敗", "失敗しました", "OK");
            }
            foreach (var downloadedPath in downloadedPaths) {
                var savedPath = downloadedPath.Key;
                File.Delete(savedPath);
            }

            // 終わり
            EditorUtility.ClearProgressBar();
        }

        // ダウンロードした日付
        [Serializable]
        class TimeStamp
        {
            public string FileName;
            public string ModifiedTime;
        }

        // ダウンロードした日付リスト
        [Serializable]
        class TimeStamps
        {
            public List<TimeStamp> Stamps;
        }

        // ダウンロード実体
        bool DownloadCore(Dictionary<string, string> downloadedPaths, bool full)
        {
            // DriveAPIサービス取得
            var service = SetupServie();

            // ダウンロードしたファイルの置き場所
            string excelsPath = Path.GetDirectoryName(Application.dataPath) + "/" + ExcelsPath;

            // そのフォルダがなければ生成
            if (!Directory.Exists(excelsPath)) {
                Directory.CreateDirectory(excelsPath);
                full = true;
            }

            // タイムスタンプ
            var stampsDir = Path.GetDirectoryName(Application.dataPath) + "/" + TimeStampsPath;
            var stampsPath = stampsDir + "/TimeStamps.json";
            TimeStamps savedStamps = null;
            if (!full && File.Exists(stampsPath)) {
                try {
                    var json = File.ReadAllText(stampsPath);
                    if (json != null) {
                        savedStamps = JsonUtility.FromJson<TimeStamps>(json);
                    }
                } catch {
                    Debug.LogWarning("ダウンロード履歴がありません");
                }
            }

            // format
            var format = "yyyy-MM-ddTHH:mm:ss zzz";

            // フォルダ内のファイル一覧取得
            FilesResource.ListRequest listRequest = service.Files.List();
            listRequest.PageSize = 1000;
            listRequest.Fields = "nextPageToken, files(id, name, modifiedTime)";
            listRequest.Q = string.Format("'{0}' in parents and trashed=false and mimeType='application/vnd.google-apps.spreadsheet'", FolderId);
            var files = listRequest.Execute().Files;
            var saveStamps = new TimeStamps();
            saveStamps.Stamps = new List<TimeStamp>();
            if (files != null && files.Count > 0) {
                for (int i = 0; i < files.Count; i++) {
                    var file = files[i];
                    var saveStamp = new TimeStamp();
                    saveStamp.FileName = file.Name;
                    saveStamp.ModifiedTime = file.ModifiedTime.Value.ToString(format);
                    saveStamps.Stamps.Add(saveStamp);
                    if (savedStamps != null && savedStamps.Stamps != null) {
                        var savedStamp = savedStamps.Stamps.Find((s) => s.FileName == file.Name);
                        if (savedStamp != null && savedStamp.ModifiedTime != null) {
                            var savedDT = DateTime.ParseExact(savedStamp.ModifiedTime, format, System.Globalization.CultureInfo.CurrentCulture);
                            var saveDT = DateTime.ParseExact(saveStamp.ModifiedTime, format, System.Globalization.CultureInfo.CurrentCulture);
                            if (savedDT >= saveDT) {
                                continue;
                            }
                        }
                    }
                    EditorUtility.DisplayProgressBar("処理中", file.Name + "をダウンロードしています...", (float)i / files.Count);
                    Debug.Log("スプレッドシートをダウンロードします:" + file.Name + " id:" + file.Id);
                    var (desiredPath, savedPath) = DownloadExcel(service, excelsPath, file.Id, file.Name);
                    if (savedPath == null) {
                        Debug.LogError("ダウンロード失敗");
                        return false;
                    }
                    downloadedPaths.Add(savedPath, desiredPath);
                    Debug.Log("Add key:" + savedPath + " value:" + desiredPath);
                }
            }
            Debug.Log("ダウンロード成功ファイル数:" + downloadedPaths.Count);

            // ファイルをリネーム
            foreach (var downloadedPath in downloadedPaths) {
                var savedPath = downloadedPath.Key;
                var desiredPath = downloadedPath.Value;
                File.Copy(savedPath, desiredPath, true);
            }

            // タイムスタンプを保存
            var saveStampJson = JsonUtility.ToJson(saveStamps);
            try {
                if (!Directory.Exists(stampsDir)) {
                    Directory.CreateDirectory(stampsDir);
                }
                File.WriteAllText(stampsPath, saveStampJson, System.Text.Encoding.UTF8);
            } catch {
                Debug.LogWarning("ダウンロード履歴の保存に失敗しました");
            }

            // 成功
            return true;
        }

        // Upload
        protected void Upload()
        {
            // 確認
            if (!EditorUtility.DisplayDialog("確認", "GoogleDriveにスプレッドシートをアップロードします", "Yes", "No")) {
                return;
            }
            if (!EditorUtility.DisplayDialog("確認", "GoogleDriveにある既存のスプレッドシートは削除され新しいファイルに差し変わりますがよろしいですか", "Yes", "No")) {
                return;
            }

            // 処理中...
            EditorUtility.DisplayProgressBar("処理中", "GoogleAPIを設定しています...", 0);

            // 処理
            try {
                // ダウンロードしたファイルの置き場所
                string excelsPath = Path.GetDirectoryName(Application.dataPath) + "/" + ExcelsPath;

                // エクセルリスト取得
                var excelPaths = Directory.GetFiles(excelsPath, "*.xlsx");

                // サービス取得
                var service = SetupServie();

                // 同名ファイルを削除
                DeleteCore(service, excelPaths);

                // アップロード
                UploadCore(service, excelPaths);
            } catch {
                EditorUtility.DisplayDialog("失敗", "失敗しました", "OK");
            }

            // 終わり
            EditorUtility.ClearProgressBar();
        }

        // GoogleDriveサービスのセットアップ
        DriveService SetupServie()
        {
            UserCredential credential;

            // GoogleAPI設定
            string[] Scopes = { DriveService.Scope.Drive, };
            using (var stream0 = new FileStream(ClientSecretJsonPath, FileMode.Open, FileAccess.Read)) {
                var credPath = Path.Combine(StoreCredentialsPath, "credentials");
                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.FromStream(stream0).Secrets,
                    Scopes,
                    "user",
                    CancellationToken.None,
                    new FileDataStore(credPath, true)).Result;
                Debug.Log("Credential file saved to: " + credPath);
            }

            // DriveAPIサービス取得
            var service = new DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = GoogleApplicationName,
            });

            // サービスを返す
            return service;
        }

        // アップロード実体
        bool UploadCore(DriveService service, string[] excelPaths)
        {
            // アップロード
            for (int i = 0; i < excelPaths.Length; i++) {
                var excelPath = excelPaths[i];

                // アップロード
                {
                    Google.Apis.Upload.IUploadProgress prog;
                    FileStream fsu = new FileStream(excelPath, FileMode.Open);
                    try {
                        Google.Apis.Drive.v3.Data.File meta = new Google.Apis.Drive.v3.Data.File();
                        meta.Name = Path.GetFileNameWithoutExtension(excelPath);
                        meta.MimeType = "application/vnd.google-apps.spreadsheet";
                        meta.Parents = new List<string>() { FolderId, };

                        EditorUtility.DisplayProgressBar("処理中", meta.Name + "をアップロードしています...", (float)i / excelPaths.Length);
                        FilesResource.CreateMediaUpload req = service.Files.Create(meta, fsu, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                        req.Fields = "id, name";
                        prog = req.Upload();
                    } finally {
                        fsu.Close();
                    }
                }
            }

            // 成功
            return true;
        }

        // Delete
        bool DeleteCore(DriveService service, string[] excelPaths)
        {
            // フォルダ内のファイル一覧取得
            FilesResource.ListRequest listRequest = service.Files.List();
            listRequest.PageSize = 1000;
            listRequest.Fields = "nextPageToken, files(id, name)";
            listRequest.Q = string.Format("'{0}' in parents and trashed=false and mimeType='application/vnd.google-apps.spreadsheet'", FolderId);
            var files = listRequest.Execute().Files;
            if (files != null && files.Count > 0) {
                for (int i = 0; i < files.Count; i++) {
                    var file = files[i];
                    bool shouldDelete = false;
                    foreach (var excelPath in excelPaths) {
                        if (Path.GetFileNameWithoutExtension(excelPath) == file.Name) {
                            shouldDelete = true;
                            break;
                        }
                    }
                    if (!shouldDelete) {
                        continue;
                    }
                    EditorUtility.DisplayProgressBar("処理中", "同名ファイル" + file.Name + "を削除しています...", (float)i / files.Count);
                    Debug.Log("スプレッドシートを削除します:" + file.Name + " id:" + file.Id);
                    var request = service.Files.Delete(file.Id);
                    request.Execute();
                }
            }

            // 成功
            return true;
        }
    }
}

あとはSampleMasterDownloaderクラスの
GoogleApplicationNameFolderId
自分の環境に合わせて書き換えます。

これでUnityのメニューに「Utils」が追加され、
実行できてめでたしめでたし...とは行かない。
その前にアクセス認証などの事前準備を行う必要があります。

事前準備リスト

  1. GoogleDriveにプログラムからアクセス可能にする
  2. UnityにGoogleAPI呼び出しdllをインポート

GoogleDriveにプログラムからアクセス可能にする

  1. ブラウザでGoogle Developer Consoleにアクセス

  2. プロジェクトを作成。
    下記ではプロジェクト名を
    SampleMasterDownloader.GoogleApplicationName
    に合わせてExcelDownloaderとしていま...スペルミスしてるぅ
    createproj.png

  3. OAuth同意画面作成

    • 最初の画面、UserTypeは外部。
    • 二番目の画面
      • アプリ名はここではExcelDownloader
      • ユーザーサポートメールとデベロッパーの連絡先は自分のメアド
      • その他は空欄のまま
    • 三番目の画面(スコープ)、何もいじらず次へ。
    • 四番目の画面(テストユーザー)、何もいじらず次へ。
    • 最後の画面で一番下にあるダッシュボードへ戻るをクリック。
  4. DriveAPIとSheetsAPIを有効化
    enableapi.png
    この辺の画面から+APIとサービスの有効化をクリックし、
    その後の画面でDriveSheetsを検索して有効化します。

  5. 認証設定をしてclient_secret.jsonをダウンロードし、MasterDownloader.csと同じフォルダに配置
    auth.png
    +認証情報を作成をクリックしてOAuthクライアントIDを作成。

    その後に出てくる画面は適当に。
    アプリケーションの種類は「デスクトップアプリ」でいけそう。
    作成したらjsonをダウンロードするボタンがどっかにあるのでクリック。

    client_secretウンタラカンタラ.jsonみたいなファイルがダウンロードされるので、
    これをclient_secret.jsonにリネームして、MasterDownloader.csと同じ場所に置く。

UnityにGoogleAPI呼び出しdllをインポート

これがまた面倒。
要は.dllが欲しいだけで、パッケージのインストールとかはしない。

以下はMacのVisualStudio for Mac Communityでの手順です。

  1. ひとまず、UnityエディタからVisualStudioを立ち上げる。

  2. VSのメニューからプロジェクト>Manage NuGet Packagesをクリック。
    出てくるダイアログから
    Google.Apis Google.Apis.Auth Google.Apis.Drive.V3を追加。
    どのプロジェクトに追加するか聞かれたら
    「Assembly.CSharp.Editor」が良さげ。
    スクリーンショット 2022-12-16 14.39.22.png

  3. 追加したら、Unityのプロジェクト直下のPackagesフォルダに
    それぞれのフォルダができてるので、
    その中にある.dllを全てMasterDownloader.csと同じフォルダか
    その下にフォルダ掘るかしてコピー。
    .dll全部コピーし終わったら
    もうPackagesフォルダの中に追加されたものは不要なので削除。

  4. .dllの.metaファイルを設定。
    Unityを立ち上げて下のように設定してApply
    スクリーンショット 2022-12-16 22.17.03.png

以上。これでスプレッドシートをエクセルとしてダウンロードできるはず。

なお、ユーザーが初めてアクセスする際、
ブラウザが立ち上がって認証許可を求められるので
許可を選んでください。結果はcredentialsに保存されるので、
二回目からは確認なしでダウンロード&アップロードできます。

また、このcredentialsは開発チームで共有する必要ないので、
Unityプロジェクト直下にできるSpreadSheetsフォルダと共に
.gitignoreに突っ込んでおきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?