【flutter_sound × Go】音声ファイルをAPI経由でAWS S3にアップロード
はじめに
以前こちらの記事で、「flutter_soundを用いた音声録音再生機能の実装」について記載した。
今回は、続きとして、録音した音声ファイルをAPI通信でアプリからサーバー(AWS S3)にアップロードする部分を実装したいと思う。
API設計
/api/upload_voice_with_schedule
エンドポイント: -
メソッド:
POST
-
リクエストボディ:
-
phrase
: フレーズテキスト (string) -
is_private
: フレーズの公開設定 (bool) -
time
: 再生時間(オプション、types.HourMinute
) -
days
: 再生曜日のリスト(オプション、[]string
) -
file
: 音声ファイル (multipart/form-data)
-
処理の流れ
- クライアントからのリクエストを受け取る。
- 音声ファイルをS3にアップロードし、
voice_path
を取得。 - フレーズのレコードをデータベースに作成。
- スケジュールが存在する場合は、スケジュールレコードを作成。
サーバー側(Go)
UserPhraseHandler
関数の実装
新しい音声ファイルアップロード用のハンドラHandleUploadVoiceWithSchedule
関数を作成
コンテキストと依存関係の管理
-
UserPhraseHandler
構造体は、データベース接続(sqlx.DB
)と設定(Config
)を保持する。これにより、APIハンドラーがデータベースや設定にアクセスできるようになる。
音声ファイルの処理
-
c.FormFile("voice")
を使用して音声ファイルを取得し、file.Open()
でファイルを開く。その後、buf.ReadFrom(src)
でファイルデータをバイトスライスに読み込む。
S3へのアップロード
- 現在の日時を使用して一意のファイル名を生成し、
UploadStreamToS3
関数を使用して音声データをS3にアップロードする。S3バケット名とファイルキーは事前に定義されている。
トランザクションの開始とフレーズの作成
- データベーストランザクションを開始し、
CreateUserPhrase
を呼び出して新しいフレーズをデータベースに作成する。失敗した場合はトランザクションをロールバックする。
音声ファイルパスの更新
- アップロードされた音声ファイルのS3パスをデータベース内のフレーズに関連付けます。ここで、
voice_path
フィールドを更新する。
APIレスポンスの送信
- 正常に完了した場合、HTTPステータス201(Created)とともに新しいフレーズのデータをJSON形式でクライアントに返す。
user_phrase_handler.go
// 音声ファイルをアプリからアップロード
func (h *UserPhraseHandler) HandleUploadVoiceWithSchedule(c echo.Context) error {
phraseText := c.FormValue("phrase")
if phraseText == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Phrase text is required")
}
uid := c.Get("uid").(string)
user, err := GetUser(h.db, uid)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "User not found")
}
file, err := c.FormFile("voice")
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Voice file is required")
}
src, err := file.Open()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Unable to open file")
}
defer src.Close()
var buf bytes.Buffer
_, err = buf.ReadFrom(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file")
}
audioData := buf.Bytes()
ctx := context.Background()
timestamp := time.Now().Format("20060102-150405")
fileName := fmt.Sprintf("user_upload_%s.mp3", timestamp)
key := fmt.Sprintf("users/%d/user_phrases/%s", user.ID, fileName)
err = UploadStreamToS3(ctx, "your-bucket-name", key, audioData)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload to S3")
}
tx, err := h.db.Beginx()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to start transaction")
}
phrase, err := CreateUserPhrase(tx, user.ID, phraseText, true, true)
if err != nil {
tx.Rollback()
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create phrase")
}
err = UpdateUserPhraseVoicePath(tx, phrase.ID, user.ID, key)
if err != nil {
tx.Rollback()
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update voice path")
}
var schedule *PhraseSchedule
// スケジュール情報の保存(存在する場合)
var req struct {
Time *types.HourMinute `json:"time,omitempty"`
Days *[]string `json:"days,omitempty"`
}
if err := c.Bind(&req); err == nil {
if req.Time != nil && req.Days != nil {
schedule, err = CreatePhraseSchedule(tx, user.ID, phrase.ID, *req.Time, *req.Days)
if err != nil {
tx.Rollback()
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create phrase schedule")
}
}
}
if err := tx.Commit(); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to commit transaction")
}
return c.JSON(http.StatusCreated, map[string]interface{}{
"phrase": phrase,
"schedule": schedule,
})
}
APIエンドポイント作成
POST
メソッドを使用して/upload_voice_with_schedule
というパスに対するリクエストを受け付けるルートを定義。先ほど作成した、HandleUploadVoiceWithSchedule関数を呼び出す。
appGroup.POST("/upload_voice_with_schedule", uph.HandleUploadVoiceWithSchedule)
アプリ側(Flutter)
uploadVoiceWithSchedule
関数:音声ファイルと関連情報をサーバーに送信
http.MultipartRequest
の使用
- 音声ファイルを含むデータを送信するために、
http.MultipartRequest
を使用する。このクラスは、ファイルなどのバイナリデータと他のフォームデータを同時に送信するためのもの。 -
request.fields
で、フォームデータとしてフレーズや公開設定の情報が設定。 -
request.files.add
で、音声ファイル(ローカルファイルのパス)を送信データに追加。
api_client.dart
api_client.dart
はサーバーとの通信を行い、APIリクエストを処理する。
Future<void> uploadVoiceWithSchedule(String phrase, String filePath,
bool isPrivate, HourMinute? time, List<String>? days) async {
final url = Uri.parse('$apiUrl/upload_voice_with_schedule');
final headers = await apiHeaders();
final request = http.MultipartRequest('POST', url)
..headers.addAll(headers)
..fields['phrase'] = phrase
..fields['is_private'] = isPrivate.toString()
..fields['recorded'] = 'true'
..files.add(await http.MultipartFile.fromPath('voice', filePath));
if (time != null) {
request.fields['time'] = jsonEncode(time.toJson());
}
if (days != null && days.isNotEmpty) {
request.fields['days'] = jsonEncode(days);
}
final response = await request.send();
if (response.statusCode != 201) {
final responseBody = await response.stream.bytesToString();
throw Exception('Failed to upload voice and phrase: $responseBody');
}
}
user_phrase_notifier.dart
user_phrase_notifier.dart
はアプリケーションの状態管理を担当し、ユーザーインターフェースに最新のデータを反映させる役割を果たす。
Future<void> uploadVoiceWithSchedule(String phrase, String filePath,
bool isPrivate, HourMinute? time, List<String>? days) async {
try {
await apiClient.uploadVoiceWithSchedule(
phrase, filePath, isPrivate, time, days);
await loadUserPhrases();
} catch (e) {
throw Exception('Failed to upload voice and schedule: $e');
}
}
音声ファイルパスを取得して送信
音声録音を行う「RecordVoiceScreen」から音声ファイルのパスを取得し、「AddPhraseScreen」でそのファイルパスを使用してAPIにデータを送信する。
lib/screens/home/record_voice_screen.dart(音声録音再生画面)
まず、「完了」ボタンを押したときに、音声ファイルパスを「AddPhraseScreen」に渡す。
void _onComplete() {
Navigator.of(context).pop(_recordedFile?.path);
}
lib/screens/home/add_phrase_screen.dart(フレーズ追加画面)
ユーザーが音声ファイルをアップロードした場合、そのファイルのパスが _recordedFilePath
に保存される。
フレーズの保存・更新の処理
- 音声ファイルがある場合は、
uploadVoiceWithSchedule
メソッドを使用して音声ファイルと一緒にフレーズをアップロードする。 - 音声ファイルがない場合は、
addUserPhraseWithSchedule
またはupdateUserPhraseWithSchedule
メソッドを呼び出して、フレーズのみをサーバーに送信する。
続きは、こちらで記載しています。
Discussion