Closed40

もくまっち開発 Week 5

光岡 高宏光岡 高宏

中身はこんな感じ。
たぶん一般的なFlutterプロジェクト何だと思う

光岡 高宏光岡 高宏

Custom Function は内部的には 1 つのファイルにまとめられてるっぽい

ということは別の Custom Function は普通に関数として呼べるかも

光岡 高宏光岡 高宏

ひとまず、 Web アプリとしてビルドしてみる

環境

$ flutter --version

Flutter 3.13.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 6c4930c4ac (3 months ago)2023-10-18 10:57:55 -0500
Engine • revision 767d8c75e8
Tools • Dart 3.1.4 • DevTools 2.25.0

実行したコマンド

$ flutter build web
Resolving dependencies... (2.9s)
+ _flutterfire_internals 1.3.16
+ async 2.11.0
+ auto_size_text 3.0.0
+ boolean_selector 2.1.1
+ cached_network_image 3.2.1 (3.3.1 available)
+ cached_network_image_platform_interface 1.0.0 (4.0.0 available)
+ cached_network_image_web 1.0.1 (1.1.1 available)
+ characters 1.3.0
+ clock 1.1.1
+ cloud_firestore 4.13.1 (4.14.0 available)
+ cloud_firestore_platform_interface 6.0.5 (6.1.0 available)
+ cloud_firestore_web 3.8.5 (3.9.0 available)
+ collection 1.17.2 (1.18.0 available)
+ crypto 3.0.3
+ cupertino_icons 1.0.6
+ fake_async 1.3.1
+ ffi 2.1.0
+ file 7.0.0
+ firebase_core 2.24.2
+ firebase_core_platform_interface 5.0.0
+ firebase_core_web 2.10.0
+ firebase_performance 0.9.3+8
+ firebase_performance_platform_interface 0.1.4+16
+ firebase_performance_web 0.1.4+16
+ floating_bottom_navigation_bar 1.5.2
+ flutter 0.0.0 from sdk flutter
+ flutter_animate 4.1.1+1 (4.3.0 available)
+ flutter_blurhash 0.7.0 (0.8.2 available)
+ flutter_cache_manager 3.3.1
+ flutter_lints 3.0.0 (3.0.1 available)
+ flutter_localizations 0.0.0 from sdk flutter
+ flutter_test 0.0.0 from sdk flutter
+ flutter_web_plugins 0.0.0 from sdk flutter
+ font_awesome_flutter 10.6.0
+ from_css_color 2.0.0
+ geolocator 10.0.1 (10.1.0 available)
+ geolocator_android 4.2.4 (4.4.0 available)
+ geolocator_apple 2.2.7 (2.3.3 available)
+ geolocator_platform_interface 4.0.8 (4.2.0 available)
+ geolocator_web 2.1.6 (2.2.0 available)
+ geolocator_windows 0.2.0 (0.2.2 available)
+ go_router 7.1.1 (13.0.1 available)
+ google_fonts 4.0.3 (6.1.0 available)
+ http 0.13.6 (1.1.2 available)
+ http_parser 4.0.2
+ intl 0.18.1 (0.19.0 available)
+ iregexp 0.1.1 (0.1.2 available)
+ js 0.6.7 (0.7.0 available)
+ json_path 0.6.2 (0.7.0 available)
+ lints 3.0.0
+ logging 1.2.0
+ matcher 0.12.16 (0.12.16+1 available)
+ material_color_utilities 0.5.0 (0.8.0 available)
+ maybe_just_nothing 0.5.3
+ meta 1.9.1 (1.11.0 available)
+ nested 1.0.0
+ octo_image 1.0.2 (2.0.0 available)
+ page_transition 2.1.0
+ path 1.8.3 (1.9.0 available)
+ path_provider 2.0.14 (2.1.1 available)
+ path_provider_android 2.0.25 (2.2.2 available)
+ path_provider_foundation 2.2.2 (2.3.1 available)
+ path_provider_linux 2.1.11 (2.2.1 available)
+ path_provider_platform_interface 2.0.6 (2.1.1 available)
+ path_provider_windows 2.1.7 (2.2.1 available)
+ petitparser 5.4.0 (6.0.2 available)
+ platform 3.1.4
+ plugin_platform_interface 2.1.6 (2.1.8 available)
+ provider 6.0.5 (6.1.1 available)
+ rfc_6901 0.1.1 (0.2.0 available)
+ rxdart 0.27.7
+ shared_preferences 2.2.2
+ shared_preferences_android 2.2.1
+ shared_preferences_foundation 2.3.4
+ shared_preferences_linux 2.3.2
+ shared_preferences_platform_interface 2.3.1
+ shared_preferences_web 2.2.1 (2.2.2 available)
+ shared_preferences_windows 2.3.2
+ sky_engine 0.0.99 from sdk flutter
+ source_span 1.10.0
+ sqflite 2.2.6 (2.3.0 available)
+ sqflite_common 2.5.0+2
+ stack_trace 1.11.0 (1.11.1 available)
+ stream_channel 2.1.1 (2.1.2 available)
+ stream_transform 2.1.0
+ string_scanner 1.2.0
+ synchronized 3.1.0+1
+ term_glyph 1.2.1
+ test_api 0.6.0 (0.7.0 available)
+ timeago 3.2.2 (3.6.0 available)
+ typed_data 1.3.2
+ url_launcher 6.1.10 (6.2.2 available)
+ url_launcher_android 6.0.27 (6.2.1 available)
+ url_launcher_ios 6.1.4 (6.2.2 available)
+ url_launcher_linux 3.0.6 (3.1.1 available)
+ url_launcher_macos 3.0.7 (3.1.0 available)
+ url_launcher_platform_interface 2.1.2 (2.3.0 available)
+ url_launcher_web 2.1.0 (2.2.3 available)
+ url_launcher_windows 3.0.8 (3.1.1 available)
+ uuid 3.0.7 (4.3.1 available)
+ vector_math 2.1.4
+ web 0.1.4-beta (0.4.0 available)
+ win32 5.1.1 (5.2.0 available)
+ xdg_directories 1.0.4
Changed 104 dependencies!

Upgrading .gitignore
Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 283452 to 1272
bytes (99.6% reduction). Tree-shaking can be disabled by providing the
--no-tree-shake-icons flag when building your app.
Font asset "fa-brands-400.ttf" was tree-shaken, reducing it from 189684 to 1532 bytes
(99.2% reduction). Tree-shaking can be disabled by providing the
--no-tree-shake-icons flag when building your app.
Font asset "fa-solid-900.ttf" was tree-shaken, reducing it from 394668 to 1652 bytes
(99.6% reduction). Tree-shaking can be disabled by providing the
--no-tree-shake-icons flag when building your app.
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to
7680 bytes (99.5% reduction). Tree-shaking can be disabled by providing the
--no-tree-shake-icons flag when building your app.
Compiling lib/main.dart for the Web...                             26.1s
光岡 高宏光岡 高宏

Android と iOS のビルドは下記コマンドっぽい

$ flutter build apk
$ flutter build ios
$ flutter build -h

Available subcommands:
  aar               Build a repository containing an AAR and a POM file.
  apk               Build an Android APK file from your app.
  appbundle         Build an Android App Bundle file from your app.
  bundle            Build the Flutter assets directory from your app.
  ios               Build an iOS application bundle (macOS host only).
  ios-framework     Produces .xcframeworks for a Flutter project and its plugins for integration into existing, plain iOS Xcode projects.
  ipa               Build an iOS archive bundle and IPA for distribution (macOS host only).
  macos             Build a macOS desktop application.
  macos-framework   Produces .xcframeworks for a Flutter project and its plugins for integration into existing, plain macOS Xcode projects.
  web               Build a web application bundle.
光岡 高宏光岡 高宏

Android のビルドをしてみる

$ flutter build apk

Android SDK がないと怒られるのでインストールする

[!] No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.
光岡 高宏光岡 高宏

Docker に手こずりまくったので、一旦 Mac ローカルで環境構築

  • Android Studio インストール & PATH を通す(.zshrcに下記を追記)
      export ANDROID_HOME=/Users/{ユーザー名}/Library/Android/sdk
      export ANDROID_SDK_ROOT=/Users/{ユーザー名}/Library/Android/sdk
      export PATH=$PATH:$ANDROID_HOME/tools:ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools
  • Flutter/Dart は Nix で用意
    • nix-shell -p flutter dart

光岡 高宏光岡 高宏

apk ファイルにビルドしてみる

flutter build apk
Upgrading build.gradle
Checking the license for package Android SDK Tools in /Users/nix/Library/Android/sdk/licenses
License for package Android SDK Tools accepted.
Preparing "Install Android SDK Tools (revision: 26.1.1)".
"Install Android SDK Tools (revision: 26.1.1)" ready.
Installing Android SDK Tools in /Users/nix/Library/Android/sdk/tools
"Install Android SDK Tools (revision: 26.1.1)" complete.
"Install Android SDK Tools (revision: 26.1.1)" finished.
Checking the license for package Android SDK Build-Tools 30.0.3 in /Users/nix/Library/Android/sdk/licenses
License for package Android SDK Build-Tools 30.0.3 accepted.
Preparing "Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)".
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" ready.
Installing Android SDK Build-Tools 30.0.3 in /Users/nix/Library/Android/sdk/build-tools/30.0.3
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" complete.
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" finished.
Checking the license for package Android SDK Platform 33 in /Users/nix/Library/Android/sdk/licenses
License for package Android SDK Platform 33 accepted.
Preparing "Install Android SDK Platform 33 (revision: 3)".
"Install Android SDK Platform 33 (revision: 3)" ready.
Installing Android SDK Platform 33 in /Users/nix/Library/Android/sdk/platforms/android-33
"Install Android SDK Platform 33 (revision: 3)" complete.
"Install Android SDK Platform 33 (revision: 3)" finished.
Checking the license for package Android SDK Platform 31 in /Users/nix/Library/Android/sdk/licenses
License for package Android SDK Platform 31 accepted.
Preparing "Install Android SDK Platform 31 (revision: 1)".
"Install Android SDK Platform 31 (revision: 1)" ready.
Installing Android SDK Platform 31 in /Users/nix/Library/Android/sdk/platforms/android-31
"Install Android SDK Platform 31 (revision: 1)" complete.
"Install Android SDK Platform 31 (revision: 1)" finished.
Font asset "fa-solid-900.ttf" was tree-shaken, reducing it from 394668 to 1652 bytes (99.6% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 283452 to 912 bytes (99.7% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 1468 bytes (99.9% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "fa-brands-400.ttf" was tree-shaken, reducing it from 189684 to 1532 bytes (99.2% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
注意:/Users/nix/.pub-cache/hosted/pub.dev/cloud_firestore-4.13.1/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.javaは推奨されないAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
注意:入力ファイルの操作のうち、未チェックまたは安全ではないものがあります。
注意:詳細は、-Xlint:uncheckedオプションを指定して再コンパイルしてください。
注意:/Users/nix/.pub-cache/hosted/pub.dev/geolocator_android-4.2.4/android/src/main/java/com/baseflow/geolocator/location/LocationManagerClient.javaは推奨されないAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
Running Gradle task 'assembleRelease'...                          228.7s
✓  Built build/app/outputs/flutter-apk/app-release.apk (23.8MB).

注意って出てるけど、ビルドできたっぽい

光岡 高宏光岡 高宏

次は iOS にチャレンジ

flutter build ios
Application not configured for iOS
make: *** [Makefile:7: build-ios] Error 1

あえなく失敗。設定されてないらしい。

光岡 高宏光岡 高宏

Web はもともとできてたけど、改めてできることを確認

flutter build web
Font asset "fa-solid-900.ttf" was tree-shaken, reducing it from 394668 to 1652 bytes (99.6% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 283452 to 1272 bytes (99.6% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "fa-brands-400.ttf" was tree-shaken, reducing it from 189684 to 1532 bytes (99.2% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7680 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Compiling lib/main.dart for the Web...                           1,985ms
光岡 高宏光岡 高宏

Flutter のビルド状況

  • Android
  • iOS
  • Web

iOS は来週
あと GitHub Actions で自動化したい

光岡 高宏光岡 高宏

ここから Google Drive のシーシャ画像を Google Drive API を使って取得していく

光岡 高宏光岡 高宏

クイックスタートのコードをちょっと変更

  • .js.ts
  • 型エラーになるところを無視(直したい)
  • getInstance,listFile,getFileを追加
import { GoogleApis, drive_v3 } from "googleapis";

const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const { authenticate } = require('@google-cloud/local-auth');
const { google } = require('googleapis');

/**
 * @var {drive_v3.Drive}
 */
let instance: any = null;

// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');

/**
 * Reads previously authorized credentials from the save file.
 *
 * @return {Promise<OAuth2Client|null>}
 */
async function loadSavedCredentialsIfExist() {
  try {
    const content = await fs.readFile(TOKEN_PATH);
    const credentials = JSON.parse(content);
    return google.auth.fromJSON(credentials);
  } catch (err) {
    return null;
  }
}

/**
 * Serializes credentials to a file comptible with GoogleAUth.fromJSON.
 *
 * @param {OAuth2Client} client
 * @return {Promise<void>}
 */
// FIXME: 型
// @ts-ignore
async function saveCredentials(client) {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content);
  const key = keys.installed || keys.web;
  const payload = JSON.stringify({
    type: 'authorized_user',
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
  });
  await fs.writeFile(TOKEN_PATH, payload);
}

/**
 * Load or request or authorization to call APIs.
 *
 */
async function authorize() {
  let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }
  client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });
  if (client.credentials) {
    await saveCredentials(client);
  }
  return client;
}

const getInstance = async (): Promise<drive_v3.Drive> => {
  // FIXME: 型
  // @ts-ignore
  if (!instance) {
    const client = await authorize();
    instance = google.drive({ version: 'v3', auth: client });
  }

  return instance;
}

export async function listFiles() {
  const drive = await getInstance();
  const res = await drive.files.list({
    pageSize: 10,
    fields: 'nextPageToken, files(id, name)',
  });
  const files = res.data.files;
  if (!files || files.length === 0) {
    console.log('No files found.');
    return;
  }

  console.log('Files:');
  files.map((file) => {
    console.log(`${file.name} (${file.id})`);
  });

  return files;
}

export async function getFile(id: string) {
  const drive = await getInstance();
  const file = await drive.files.get({
    fileId: id,
    alt: 'media'
  });
  console.log(file.data.name);
}
光岡 高宏光岡 高宏

index.tsから上記のlistFilesを呼び出すとマイドライブのファイルリストが取得できた

bun run src/index.ts
Files:
work-room-2022 (14wV1VeFeCVcUZDQybJDnNxxxxxxxxxxxxxxx)
bed-room-2022 (1udSd3LuaR9MYU1k2kK05KeSrhO5xxxxxxxxxxxxx)
為替、VOO、VTI (1XyJ4k_JnN5bplGlwbVBxa2Jj5rNO2xxxxxxxxxxxxxxx)
UkkaReminder-release.apk (1UoFdSIqDQuT9LuyRCpxxxxxxxxxxxxxxx)
1847C181-3669-4FBC-AEC3-413DC63EA203.png (1B51xvgluhXOX7QE_0xxxxxxxxxxxxx)
もくまっち (1vWiqHwgf5V0RXK8nsxxxxxxxxxxxxxxxxxxx)
app-release.apk.sha1 (1JdU1nYnJhy5dBqp_sYD8Wxxxxxxxxxx)
app-release.apk (1KlCeUM33hOg5Hx8e6E616xxxxxxxxxxx)
もくまっち(回答) のコピー (1Ffvt2OiVrDpScJrFUmjWByuBwMqs7x0-xxxxxxxxxx)
mochmatch (1RdRM5ugCDudY6PD9pm_k4Uxxxxxxxxxx)
光岡 高宏光岡 高宏

getFileも呼んでみるが、エラーになる。
どうやら権限がない様子。

14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | exports.GaxiosError = void 0;
16 | /* eslint-disable @typescript-eslint/no-explicit-any */
17 | class GaxiosError extends Error {
18 |     constructor(message, options, response) {
19 |         super(message);
             ^
error: The user has not granted the app 334958xxxxxx read access to the file 1UoFdSIqDQuT9LuyRCpB1mIKxxxxxxxxxx.
 errors: [
  {
    "message": "The user has not granted the app 3349583xxxxx read access to the file 1UoFdSIqDQuT9LuyRCpB1mIxxxxxxxxxx.",
    "domain": "global",
    "reason": "appNotAuthorizedToFile",
    "location": "Authorization",
    "locationType": "header"
  }
]
 code: "403"
光岡 高宏光岡 高宏

スコープを下記に変更したら解消した
https://www.googleapis.com/auth/drive.readonly

import { GoogleApis, drive_v3 } from "googleapis";

const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const { authenticate } = require('@google-cloud/local-auth');
const { google } = require('googleapis');

/**
 * @var {drive_v3.Drive}
 */
let instance: any = null;

// If modifying these scopes, delete token.json.
const SCOPES = [
-   'https://www.googleapis.com/auth/drive.metadata.readonly'
+   'https://www.googleapis.com/auth/drive.readonly',
];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');

/**
 * Reads previously authorized credentials from the save file.
 *
 * @return {Promise<OAuth2Client|null>}
 */
async function loadSavedCredentialsIfExist() {
  try {
    const content = await fs.readFile(TOKEN_PATH);
    const credentials = JSON.parse(content);
    return google.auth.fromJSON(credentials);
  } catch (err) {
    return null;
  }
}

/**
 * Serializes credentials to a file comptible with GoogleAUth.fromJSON.
 *
 * @param {OAuth2Client} client
 * @return {Promise<void>}
 */
// FIXME: 型
// @ts-ignore
async function saveCredentials(client) {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content);
  const key = keys.installed || keys.web;
  const payload = JSON.stringify({
    type: 'authorized_user',
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
  });
  await fs.writeFile(TOKEN_PATH, payload);
}

/**
 * Load or request or authorization to call APIs.
 *
 */
async function authorize() {
  let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }
  client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });
  if (client.credentials) {
    await saveCredentials(client);
  }
  return client;
}

const getInstance = async (): Promise<drive_v3.Drive> => {
  // FIXME: 型
  // @ts-ignore
  if (!instance) {
    const client = await authorize();
    instance = google.drive({ version: 'v3', auth: client });
  }

  return instance;
}

export async function listFiles() {
  const drive = await getInstance();
  const res = await drive.files.list({
    pageSize: 10,
    fields: 'nextPageToken, files(id, name)',
  });
  const files = res.data.files;
  if (!files || files.length === 0) {
    console.log('No files found.');
    return;
  }

  console.log('Files:');
  files.map((file) => {
    console.log(`${file.name} (${file.id})`);
  });

  return files;
}

export async function getFile(id: string) {
  const drive = await getInstance();
  const file = await drive.files.get({
    fileId: id,
    alt: 'media'
  });
  console.log(file.data.name);
}
光岡 高宏光岡 高宏

getFileを一部変更

export async function getFile(id: string) {
  const drive = await getInstance();
- const file = await drive.files.get({
+ const {
+   config,
+   data,
+   headers,
+   request,
+   status,
+   statusText
+ } = await drive.files.get({
+   fileId: id,
+ });

- console.log(file.data.name);
+ console.debug({
+   config,
+   data,
+   headers,
+   request,
+   status,
+   statusText
+ });
}

リクエストとレスポンスはこんな感じ
ファイルのメタデータしかないけど、どうやってダウンロードするんだろ

{
  config: {
    url: "https://www.googleapis.com/drive/v3/files/1a2IKptsGtn7gqQJrKI2udtv8zBPJVyhW",
    method: "GET",
    userAgentDirectives: [
      [Object ...]
    ],
    paramsSerializer: [Function],
    headers: {
      "x-goog-api-client": "gdcl/6.0.4 gl-node/20.8.0",
      "Accept-Encoding": "gzip",
      "User-Agent": "google-api-nodejs-client/6.0.4 (gzip)",
      Authorization: "Bearer ya29.a0AfB_byCWNTfDdu3htMaIXGEsn6S7A5s9MT-3hBq332eVOMkh8L4WZdyFKoTMjGFV9mYNYh8owZflRG5ryDeje8zyKKql3hUviiCwD_PuvX3bf9qjEwdJU00qdzzVS9rKpRXCNuKGChEiA6WPVtak-DZFUNNtrX8iQ5B1aCgYKARoSARMSFQHGX2MiOpzgX9hgnrKxKxqCbzED3g0171",
      Accept: "application/json",
    },
    params: {},
    validateStatus: [Function],
    retry: true,
    responseType: "json",
  },
  data: {
    kind: "drive#file",
    id: "1a2IKptsGtn7gqQJrKI2udtv8zBPJVyhW",
    name: "1Gwj2a77NTcjmUR79eg-aAqwlEqfld5mY.webp",
    mimeType: "image/webp",
  },
  headers: {
    "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
    "cache-control": "no-cache, no-store, max-age=0, must-revalidate",
    "content-encoding": "gzip",
    "content-type": "application/json; charset=UTF-8",
    date: "Sat, 13 Jan 2024 08:29:19 GMT",
    expires: "Mon, 01 Jan 1990 00:00:00 GMT",
    pragma: "no-cache",
    server: "ESF",
    "transfer-encoding": "chunked",
    vary: "Origin, X-Origin",
    "x-content-type-options": "nosniff",
    "x-frame-options": "SAMEORIGIN",
    "x-xss-protection": "0",
  },
  request: {
    responseURL: "https://www.googleapis.com/drive/v3/files/1a2IKptsGtn7gqQJrKI2udtv8zBPJVyhW",
  },
  status: 200,
  statusText: "OK",
}
光岡 高宏光岡 高宏

リクエストにalt=mediaをつけないといけないっぽい

ドライブに保存されている blob ファイルをダウンロードするには、ダウンロードするファイルの ID と alt=media URL パラメータを指定して files.get メソッドを使用します。alt=media URL パラメータは、コンテンツのダウンロードが代替レスポンス形式としてリクエストされていることをサーバーに伝えます。

https://developers.google.com/drive/api/guides/manage-downloads?hl=ja

光岡 高宏光岡 高宏

alt=mediaを追加

export async function getFile(id: string) {
  const drive = await getInstance();
  const {
    config,
    data,
    headers,
    request,
    status,
    statusText
  } = await drive.files.get({
    fileId: id,
 +  alt: 'media',
  });

  console.debug({
    config,
    data,
    headers,
    request,
    status,
    statusText
  });
}

レスポンスボディがバイナリになった!

光岡 高宏光岡 高宏

drive.readonly.metadata スコープを使用するアプリには、ファイルの内容をダウンロードする権限がありません。

なるほど。だから最初ダウンロードできなかったのか。

光岡 高宏光岡 高宏

違った。 Google ドキュメントファイル限定っぽい。

Google Workspace ドキュメントをリクエストされた MIME タイプにエクスポートし、エクスポートされたバイト コンテンツを返します。エクスポートできるコンテンツのサイズの上限は 10 MB です。

実行したら↓のエラーが発生

error: {
  "error": {
    "code": 403,
    "message": "Export only supports Docs Editors files.",
    "errors": [
      {
        "message": "Export only supports Docs Editors files.",
        "domain": "global",
        "reason": "fileNotExportable"
      }
    ]
  }
}
export async function exportFile(id: string, mimeType: 'application/pdf') {
  const drive = await getInstance();
  const stream = await drive.files.export({
    fileId: id,
    mimeType,
  }, {
    responseType: "stream",
  });

  return stream.data;
}
光岡 高宏光岡 高宏

ドライブに保存されている blob ファイルをダウンロードするには、ダウンロードするファイルの ID と alt=media URL パラメータを指定して files.get メソッドを使用します。

光岡 高宏光岡 高宏

ということで、getFileを以下のように書き換え

export async function getFile(id: string) {
  const drive = await getInstance();
  const stream = await drive.files.get(
    {
      fileId: id,
    },
    {
      responseType: 'stream',
    }
  );

  return {
    status: stream.status,
    readable: stream.data,
  };
}
光岡 高宏光岡 高宏

呼び出し側は下記

import { createWriteStream} from 'fs';
import { getFile } from 'getFile を含むファイルのパス';

const { readable, status } = await getFile('ダウンロードしたいファイルのID'); // listFiles などで取得
const writable = createWriteStream(`./download.webp`);
readable.pipe(writable);
光岡 高宏光岡 高宏

おぉ、ダウンロードできたっぽい

あれ、でもファイルが 0 バイトだ

光岡 高宏光岡 高宏

Firestore の更新までできた!
一応同じ写真がアップロードされてるかは別途確認しよう

光岡 高宏光岡 高宏

#もくまっち 開発 Week 6 のタスク

  • 写真の before/after 確認
  • 写真が横になってるのを縦にする
  • HEIF フォーマットの写真のアップロード
  • iOS のビルド
  • Play ストアの開発者アカウント開設

来週も頑張る!

#ミリコンバレー

光岡 高宏光岡 高宏

今週の進捗

  • Firebase Storage に画像アップロードの残り
    • 残り 1 枚まで来た
    • 合ってるか確認
  • 使い方ページの見た目更新
  • FlutterFlow のコードをダウンロードしてビルドしてみる ←追加
    • Android, Web はできた
    • iOS はまだ

どれも微妙におわってない

このスクラップは3ヶ月前にクローズされました