diff --git a/.env.template b/.env.template index 4a19a29..32ab606 100644 --- a/.env.template +++ b/.env.template @@ -21,3 +21,8 @@ AZURE_AI_SPEECH_ENDPOINT="https://.cognitiveservices.azure.com/ # Chats WebSocket # Azure Container Apps: `wss://yourcontainerapps.japaneast.azurecontainerapps.io` CHATS_WEBSOCKET_URL="ws://localhost:8000" + +# Microsoft Graph Sites +MICROSOFT_GRAPH_TENANT_ID="" +MICROSOFT_GRAPH_CLIENT_ID="" +MICROSOFT_GRAPH_CLIENT_SECRET="" diff --git a/docs/index.md b/docs/index.md index f891a0e..ea21450 100644 --- a/docs/index.md +++ b/docs/index.md @@ -169,3 +169,66 @@ az resource update \ ### Azure AI Speech - [バッチ文字起こしとは](https://learn.microsoft.com/ja-jp/azure/ai-services/speech-service/batch-transcription) + +### Microsoft Graph Sites Service + +Microsoft Graph Sites サービスは、SharePoint Online のファイルを操作するためのサービスです。 + +```shell +# Help +uv run python scripts/msgraphs/sites.py --help + +# List files in SharePoint site (site_id is required) +uv run python scripts/msgraphs/sites.py list-files + +# List files in specific folder +uv run python scripts/msgraphs/sites.py list-files --folder "Documents" + +# List files in JSON format +uv run python scripts/msgraphs/sites.py list-files --format json + +# Upload a single file +uv run python scripts/msgraphs/sites.py upload-file "local_file.txt" + +# Upload file to specific folder +uv run python scripts/msgraphs/sites.py upload-file "local_file.txt" --folder "Documents" + +# Upload multiple files +uv run python scripts/msgraphs/sites.py upload-files "file1.txt" "file2.txt" "file3.txt" + +# Upload multiple files to specific folder +uv run python scripts/msgraphs/sites.py upload-files "file1.txt" "file2.txt" --folder "Documents" + +# Download a file +uv run python scripts/msgraphs/sites.py download-file "remote_file.txt" + +# Download file from specific folder +uv run python scripts/msgraphs/sites.py download-file "remote_file.txt" --folder "Documents" + +# Download file with custom output path +uv run python scripts/msgraphs/sites.py download-file "remote_file.txt" --output "downloaded_file.txt" + +# Get file information +uv run python scripts/msgraphs/sites.py get-file-info "remote_file.txt" + +# Get file information from specific folder +uv run python scripts/msgraphs/sites.py get-file-info "remote_file.txt" --folder "Documents" + +# Delete a file +uv run python scripts/msgraphs/sites.py delete-file "remote_file.txt" + +# Delete file from specific folder +uv run python scripts/msgraphs/sites.py delete-file "remote_file.txt" --folder "Documents" + +# Delete file without confirmation +uv run python scripts/msgraphs/sites.py delete-file "remote_file.txt" --force + +# Delete multiple files +uv run python scripts/msgraphs/sites.py delete-files "file1.txt" "file2.txt" "file3.txt" + +# Delete multiple files from specific folder +uv run python scripts/msgraphs/sites.py delete-files "file1.txt" "file2.txt" --folder "Documents" + +# Delete multiple files without confirmation +uv run python scripts/msgraphs/sites.py delete-files "file1.txt" "file2.txt" --force +``` diff --git a/pyproject.toml b/pyproject.toml index 64b0f4e..23a966d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "fastapi[standard]>=0.115.12", "langchain-community>=0.3.27", "langchain-openai>=0.3.27", + "msgraph-sdk>=1.9.0", "opentelemetry-instrumentation-fastapi>=0.52b1", "pydantic-settings>=2.10.1", "typer>=0.16.0", diff --git a/scripts/msgraphs/__init__.py b/scripts/msgraphs/__init__.py new file mode 100644 index 0000000..16a16d5 --- /dev/null +++ b/scripts/msgraphs/__init__.py @@ -0,0 +1 @@ +# Microsoft Graph scripts module \ No newline at end of file diff --git a/scripts/msgraphs/sites.py b/scripts/msgraphs/sites.py new file mode 100644 index 0000000..644dbc9 --- /dev/null +++ b/scripts/msgraphs/sites.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +# filepath: /home/runner/work/template-fastapi/template-fastapi/scripts/msgraphs/sites.py + +import json +import os +from pathlib import Path + +import typer +from rich.console import Console +from rich.table import Table + +from template_fastapi.repositories.msgraphs import MicrosoftGraphRepository + +app = typer.Typer() +console = Console() +msgraphs_repo = MicrosoftGraphRepository() + + +@app.command() +def list_files( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + folder: str = typer.Option("", help="フォルダパス(省略時はルートディレクトリ)"), + format: str = typer.Option("table", help="出力形式 (table/json)"), +) -> None: + """SharePointサイトのファイル一覧を表示する""" + try: + files = msgraphs_repo.sites.list_files(site_id, folder) + + if format == "json": + files_data = [ + { + "name": file.name, + "size": file.size, + "content_type": file.content_type, + "created_at": file.created_at.isoformat() if file.created_at else None, + "updated_at": file.updated_at.isoformat() if file.updated_at else None, + } + for file in files + ] + console.print(json.dumps(files_data, indent=2)) + else: + table = Table(show_header=True, header_style="bold magenta") + table.add_column("ファイル名", style="cyan") + table.add_column("サイズ", justify="right") + table.add_column("タイプ", style="green") + table.add_column("作成日時", style="blue") + table.add_column("更新日時", style="blue") + + for file in files: + size_str = f"{file.size:,} bytes" if file.size else "不明" + created_str = file.created_at.strftime("%Y-%m-%d %H:%M:%S") if file.created_at else "不明" + updated_str = file.updated_at.strftime("%Y-%m-%d %H:%M:%S") if file.updated_at else "不明" + + table.add_row( + file.name, + size_str, + file.content_type, + created_str, + updated_str, + ) + + console.print(table) + + console.print(f"\n📁 フォルダ: {folder if folder else 'ルート'}") + console.print(f"📄 ファイル数: {len(files)}") + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +@app.command() +def upload_file( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + file_path: str = typer.Argument(..., help="アップロードするファイルのパス"), + folder: str = typer.Option("", help="アップロード先のフォルダパス"), +) -> None: + """SharePointサイトにファイルをアップロードする""" + try: + file_path_obj = Path(file_path) + if not file_path_obj.exists(): + console.print(f"❌ ファイルが見つかりません: {file_path}", style="bold red") + raise typer.Exit(1) + + with open(file_path_obj, "rb") as f: + file_content = f.read() + + uploaded_file = msgraphs_repo.sites.upload_file(site_id, file_content, file_path_obj.name, folder) + + console.print(f"✅ ファイルをアップロードしました: {uploaded_file.name}", style="bold green") + console.print(f"📁 フォルダ: {folder if folder else 'ルート'}") + console.print(f"📄 サイズ: {uploaded_file.size:,} bytes") + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +@app.command() +def upload_files( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + file_paths: list[str] = typer.Argument(..., help="アップロードするファイルのパス(複数指定可能)"), + folder: str = typer.Option("", help="アップロード先のフォルダパス"), +) -> None: + """SharePointサイトに複数のファイルを同時にアップロードする""" + try: + files_data = [] + for file_path in file_paths: + file_path_obj = Path(file_path) + if not file_path_obj.exists(): + console.print(f"❌ ファイルが見つかりません: {file_path}", style="bold red") + continue + + with open(file_path_obj, "rb") as f: + file_content = f.read() + files_data.append((file_content, file_path_obj.name)) + + if not files_data: + console.print("❌ 有効なファイルがありません", style="bold red") + raise typer.Exit(1) + + uploaded_files = msgraphs_repo.sites.upload_multiple_files(site_id, files_data, folder) + + console.print(f"✅ {len(uploaded_files)}個のファイルをアップロードしました:", style="bold green") + for file in uploaded_files: + console.print(f" 📄 {file.name} ({file.size:,} bytes)") + console.print(f"📁 フォルダ: {folder if folder else 'ルート'}") + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +@app.command() +def download_file( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + file_name: str = typer.Argument(..., help="ダウンロードするファイル名"), + folder: str = typer.Option("", help="ファイルがあるフォルダパス"), + output: str = typer.Option("", help="出力ファイルパス(省略時はファイル名を使用)"), +) -> None: + """SharePointサイトからファイルをダウンロードする""" + try: + file_content = msgraphs_repo.sites.download_file(site_id, file_name, folder) + + output_path = Path(output) if output else Path(file_name) + with open(output_path, "wb") as f: + f.write(file_content) + + console.print(f"✅ ファイルをダウンロードしました: {output_path}", style="bold green") + console.print(f"📁 フォルダ: {folder if folder else 'ルート'}") + console.print(f"📄 サイズ: {len(file_content):,} bytes") + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +@app.command() +def delete_file( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + file_name: str = typer.Argument(..., help="削除するファイル名"), + folder: str = typer.Option("", help="ファイルがあるフォルダパス"), + force: bool = typer.Option(False, "--force", "-f", help="確認なしで削除する"), +) -> None: + """SharePointサイトからファイルを削除する""" + try: + if not force: + confirm = typer.confirm(f"ファイル '{file_name}' を削除しますか?") + if not confirm: + console.print("削除をキャンセルしました", style="yellow") + return + + success = msgraphs_repo.sites.delete_file(site_id, file_name, folder) + + if success: + console.print(f"✅ ファイルを削除しました: {file_name}", style="bold green") + console.print(f"📁 フォルダ: {folder if folder else 'ルート'}") + else: + console.print(f"❌ ファイルの削除に失敗しました: {file_name}", style="bold red") + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +@app.command() +def delete_files( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + file_names: list[str] = typer.Argument(..., help="削除するファイル名(複数指定可能)"), + folder: str = typer.Option("", help="ファイルがあるフォルダパス"), + force: bool = typer.Option(False, "--force", "-f", help="確認なしで削除する"), +) -> None: + """SharePointサイトから複数のファイルを削除する""" + try: + if not force: + console.print(f"削除予定のファイル:") + for file_name in file_names: + console.print(f" 📄 {file_name}") + confirm = typer.confirm(f"{len(file_names)}個のファイルを削除しますか?") + if not confirm: + console.print("削除をキャンセルしました", style="yellow") + return + + deleted_files = msgraphs_repo.sites.delete_multiple_files(site_id, file_names, folder) + + console.print(f"✅ {len(deleted_files)}個のファイルを削除しました:", style="bold green") + for file_name in deleted_files: + console.print(f" 📄 {file_name}") + console.print(f"📁 フォルダ: {folder if folder else 'ルート'}") + + failed_count = len(file_names) - len(deleted_files) + if failed_count > 0: + console.print(f"⚠️ {failed_count}個のファイルの削除に失敗しました", style="yellow") + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +@app.command() +def get_file_info( + site_id: str = typer.Argument(..., help="SharePoint Site ID"), + file_name: str = typer.Argument(..., help="情報を取得するファイル名"), + folder: str = typer.Option("", help="ファイルがあるフォルダパス"), +) -> None: + """SharePointサイトのファイル情報を取得する""" + try: + file_info = msgraphs_repo.sites.get_file_info(site_id, file_name, folder) + + table = Table(show_header=True, header_style="bold magenta") + table.add_column("項目", style="cyan") + table.add_column("値", style="white") + + table.add_row("ファイル名", file_info.name) + table.add_row("サイズ", f"{file_info.size:,} bytes" if file_info.size else "不明") + table.add_row("タイプ", file_info.content_type) + table.add_row("作成日時", file_info.created_at.strftime("%Y-%m-%d %H:%M:%S") if file_info.created_at else "不明") + table.add_row("更新日時", file_info.updated_at.strftime("%Y-%m-%d %H:%M:%S") if file_info.updated_at else "不明") + table.add_row("フォルダ", folder if folder else "ルート") + + console.print(table) + + except Exception as e: + console.print(f"❌ エラー: {e}", style="bold red") + raise typer.Exit(1) + + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/template_fastapi/app.py b/template_fastapi/app.py index 8780361..f8ab060 100644 --- a/template_fastapi/app.py +++ b/template_fastapi/app.py @@ -11,7 +11,7 @@ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.trace import Span -from template_fastapi.routers import chats, demos, files, foodies, games, items, speeches +from template_fastapi.routers import chats, demos, files, foodies, games, items, msgraphs, speeches app = FastAPI() @@ -42,4 +42,5 @@ def server_request_hook(span: Span, scope: dict): app.include_router(foodies.router) app.include_router(files.router) app.include_router(speeches.router) +app.include_router(msgraphs.router, prefix="/msgraphs") app.include_router(chats.router) diff --git a/template_fastapi/repositories/msgraphs.py b/template_fastapi/repositories/msgraphs.py new file mode 100644 index 0000000..1e5ff6b --- /dev/null +++ b/template_fastapi/repositories/msgraphs.py @@ -0,0 +1,158 @@ +from datetime import datetime +from io import BytesIO + +from azure.identity import ClientSecretCredential +from msgraph import GraphServiceClient +from msgraph.generated.models.drive_item import DriveItem +from msgraph.generated.models.upload_session import UploadSession + +from template_fastapi.models.file import File +from template_fastapi.settings.microsoft_graph import get_microsoft_graph_settings + +# 設定の取得 +microsoft_graph_settings = get_microsoft_graph_settings() + + +class MicrosoftGraphSitesRepository: + """Microsoft Graph Sites(SharePoint)のファイルを管理するリポジトリクラス""" + + def __init__(self, graph_client: GraphServiceClient): + self.graph_client = graph_client + + def list_files(self, site_id: str, folder_path: str = "") -> list[File]: + """SharePointサイトのファイル一覧を取得する""" + try: + if folder_path: + # フォルダ内のファイルを取得 + drive_items = self.graph_client.sites.by_site_id(site_id).drive.root.item_with_path(folder_path).children.get() + else: + # ルートディレクトリのファイルを取得 + drive_items = self.graph_client.sites.by_site_id(site_id).drive.root.children.get() + + files = [] + if drive_items and drive_items.value: + for item in drive_items.value: + if item.file: # ファイルのみ(フォルダは除外) + file_obj = File( + name=item.name, + size=item.size or 0, + content_type=item.file.mime_type if item.file else "application/octet-stream", + created_at=item.created_date_time.replace(tzinfo=None) if item.created_date_time else datetime.now(), + updated_at=item.last_modified_date_time.replace(tzinfo=None) if item.last_modified_date_time else datetime.now(), + ) + files.append(file_obj) + return files + except Exception as e: + raise RuntimeError(f"ファイル一覧の取得に失敗しました: {str(e)}") + + def upload_file(self, site_id: str, file_content: bytes, file_name: str, folder_path: str = "") -> File: + """SharePointサイトにファイルをアップロードする""" + try: + # アップロード先のパスを構築 + upload_path = f"{folder_path}/{file_name}" if folder_path else file_name + + # ファイルをアップロード + uploaded_item = self.graph_client.sites.by_site_id(site_id).drive.root.item_with_path(upload_path).content.put( + file_content, + content_type="application/octet-stream" + ) + + # アップロードされたファイルの情報を取得 + return File( + name=uploaded_item.name, + size=uploaded_item.size or len(file_content), + content_type=uploaded_item.file.mime_type if uploaded_item.file else "application/octet-stream", + created_at=uploaded_item.created_date_time.replace(tzinfo=None) if uploaded_item.created_date_time else datetime.now(), + updated_at=uploaded_item.last_modified_date_time.replace(tzinfo=None) if uploaded_item.last_modified_date_time else datetime.now(), + ) + except Exception as e: + raise RuntimeError(f"ファイルのアップロードに失敗しました: {str(e)}") + + def upload_multiple_files(self, site_id: str, files_data: list[tuple[bytes, str]], folder_path: str = "") -> list[File]: + """SharePointサイトに複数のファイルを同時にアップロードする""" + uploaded_files = [] + for file_content, file_name in files_data: + uploaded_file = self.upload_file(site_id, file_content, file_name, folder_path) + uploaded_files.append(uploaded_file) + return uploaded_files + + def download_file(self, site_id: str, file_name: str, folder_path: str = "") -> bytes: + """SharePointサイトからファイルをダウンロードする""" + try: + # ファイルパスを構築 + file_path = f"{folder_path}/{file_name}" if folder_path else file_name + + # ファイルの内容を取得 + content = self.graph_client.sites.by_site_id(site_id).drive.root.item_with_path(file_path).content.get() + return content + except Exception as e: + raise RuntimeError(f"ファイルのダウンロードに失敗しました: {str(e)}") + + def delete_file(self, site_id: str, file_name: str, folder_path: str = "") -> bool: + """SharePointサイトからファイルを削除する""" + try: + # ファイルパスを構築 + file_path = f"{folder_path}/{file_name}" if folder_path else file_name + + # ファイルを削除 + self.graph_client.sites.by_site_id(site_id).drive.root.item_with_path(file_path).delete() + return True + except Exception as e: + raise RuntimeError(f"ファイルの削除に失敗しました: {str(e)}") + + def delete_multiple_files(self, site_id: str, file_names: list[str], folder_path: str = "") -> list[str]: + """SharePointサイトから複数のファイルを削除する""" + deleted_files = [] + for file_name in file_names: + try: + if self.delete_file(site_id, file_name, folder_path): + deleted_files.append(file_name) + except Exception as e: + print(f"ファイル {file_name} の削除に失敗しました: {str(e)}") + return deleted_files + + def get_file_info(self, site_id: str, file_name: str, folder_path: str = "") -> File: + """SharePointサイトのファイル情報を取得する""" + try: + # ファイルパスを構築 + file_path = f"{folder_path}/{file_name}" if folder_path else file_name + + # ファイル情報を取得 + item = self.graph_client.sites.by_site_id(site_id).drive.root.item_with_path(file_path).get() + + return File( + name=item.name, + size=item.size or 0, + content_type=item.file.mime_type if item.file else "application/octet-stream", + created_at=item.created_date_time.replace(tzinfo=None) if item.created_date_time else datetime.now(), + updated_at=item.last_modified_date_time.replace(tzinfo=None) if item.last_modified_date_time else datetime.now(), + ) + except Exception as e: + raise RuntimeError(f"ファイル情報の取得に失敗しました: {str(e)}") + + +class MicrosoftGraphRepository: + """Microsoft Graphの各種サービスを統合するリポジトリクラス""" + + def __init__(self): + self._graph_client = None + self._sites = None + + @property + def graph_client(self) -> GraphServiceClient: + """GraphServiceClientを遅延初期化するプロパティ""" + if self._graph_client is None: + credential = ClientSecretCredential( + tenant_id=microsoft_graph_settings.microsoft_graph_tenant_id, + client_id=microsoft_graph_settings.microsoft_graph_client_id, + client_secret=microsoft_graph_settings.microsoft_graph_client_secret, + ) + self._graph_client = GraphServiceClient(credentials=credential) + return self._graph_client + + @property + def sites(self) -> MicrosoftGraphSitesRepository: + """Sitesサブモジュールのプロパティ""" + if self._sites is None: + self._sites = MicrosoftGraphSitesRepository(self.graph_client) + return self._sites \ No newline at end of file diff --git a/template_fastapi/routers/msgraphs.py b/template_fastapi/routers/msgraphs.py new file mode 100644 index 0000000..42a1b38 --- /dev/null +++ b/template_fastapi/routers/msgraphs.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from template_fastapi.routers.msgraphs import sites + +router = APIRouter() + +# サブルーターを追加 +router.include_router(sites.router, prefix="/sites", tags=["msgraphs/sites"]) \ No newline at end of file diff --git a/template_fastapi/routers/msgraphs/__init__.py b/template_fastapi/routers/msgraphs/__init__.py new file mode 100644 index 0000000..f31a889 --- /dev/null +++ b/template_fastapi/routers/msgraphs/__init__.py @@ -0,0 +1 @@ +# Microsoft Graph routers module \ No newline at end of file diff --git a/template_fastapi/routers/msgraphs/sites.py b/template_fastapi/routers/msgraphs/sites.py new file mode 100644 index 0000000..394b8cd --- /dev/null +++ b/template_fastapi/routers/msgraphs/sites.py @@ -0,0 +1,159 @@ +import io + +from fastapi import APIRouter, File, HTTPException, UploadFile, Query +from fastapi.responses import StreamingResponse + +from template_fastapi.models.file import File as FileModel +from template_fastapi.repositories.msgraphs import MicrosoftGraphRepository + +router = APIRouter() +msgraphs_repo = MicrosoftGraphRepository() + + +@router.get( + "/files/", + response_model=list[FileModel], + operation_id="list_sharepoint_files", +) +async def list_sharepoint_files( + site_id: str = Query(..., description="SharePoint Site ID"), + folder_path: str = Query("", description="フォルダパス") +) -> list[FileModel]: + """ + SharePointサイトのファイル一覧を取得する + """ + try: + return msgraphs_repo.sites.list_files(site_id, folder_path) + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイル一覧の取得に失敗しました: {str(e)}") + + +@router.post( + "/files/upload", + response_model=FileModel, + operation_id="upload_sharepoint_file", +) +async def upload_sharepoint_file( + site_id: str = Query(..., description="SharePoint Site ID"), + file: UploadFile = File(...), + folder_path: str = Query("", description="アップロード先のフォルダパス") +) -> FileModel: + """ + SharePointサイトにファイルをアップロードする + """ + try: + file_content = await file.read() + return msgraphs_repo.sites.upload_file(site_id, file_content, file.filename or "unknown", folder_path) + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイルのアップロードに失敗しました: {str(e)}") + + +@router.post( + "/files/upload-multiple", + response_model=list[FileModel], + operation_id="upload_multiple_sharepoint_files", +) +async def upload_multiple_sharepoint_files( + site_id: str = Query(..., description="SharePoint Site ID"), + files: list[UploadFile] = File(...), + folder_path: str = Query("", description="アップロード先のフォルダパス") +) -> list[FileModel]: + """ + SharePointサイトに複数のファイルを同時にアップロードする + """ + try: + files_data = [] + for file in files: + file_content = await file.read() + files_data.append((file_content, file.filename or "unknown")) + + return msgraphs_repo.sites.upload_multiple_files(site_id, files_data, folder_path) + except Exception as e: + raise HTTPException(status_code=500, detail=f"複数ファイルのアップロードに失敗しました: {str(e)}") + + +@router.get( + "/files/{file_name}", + operation_id="download_sharepoint_file", +) +async def download_sharepoint_file( + file_name: str, + site_id: str = Query(..., description="SharePoint Site ID"), + folder_path: str = Query("", description="ファイルがあるフォルダパス") +) -> StreamingResponse: + """ + SharePointサイトからファイルをダウンロードする + """ + try: + file_content = msgraphs_repo.sites.download_file(site_id, file_name, folder_path) + + return StreamingResponse( + io.BytesIO(file_content), + media_type="application/octet-stream", + headers={"Content-Disposition": f"attachment; filename={file_name}"} + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイルのダウンロードに失敗しました: {str(e)}") + + +@router.get( + "/files/{file_name}/info", + response_model=FileModel, + operation_id="get_sharepoint_file_info", +) +async def get_sharepoint_file_info( + file_name: str, + site_id: str = Query(..., description="SharePoint Site ID"), + folder_path: str = Query("", description="ファイルがあるフォルダパス") +) -> FileModel: + """ + SharePointサイトのファイル情報を取得する + """ + try: + return msgraphs_repo.sites.get_file_info(site_id, file_name, folder_path) + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイル情報の取得に失敗しました: {str(e)}") + + +@router.delete( + "/files/{file_name}", + operation_id="delete_sharepoint_file", +) +async def delete_sharepoint_file( + file_name: str, + site_id: str = Query(..., description="SharePoint Site ID"), + folder_path: str = Query("", description="ファイルがあるフォルダパス") +) -> dict: + """ + SharePointサイトからファイルを削除する + """ + try: + success = msgraphs_repo.sites.delete_file(site_id, file_name, folder_path) + if success: + return {"message": f"ファイル '{file_name}' を削除しました"} + else: + raise HTTPException(status_code=500, detail="ファイルの削除に失敗しました") + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイルの削除に失敗しました: {str(e)}") + + +@router.delete( + "/files/", + operation_id="delete_multiple_sharepoint_files", +) +async def delete_multiple_sharepoint_files( + site_id: str = Query(..., description="SharePoint Site ID"), + file_names: list[str] = Query(..., description="削除するファイル名のリスト"), + folder_path: str = Query("", description="ファイルがあるフォルダパス") +) -> dict: + """ + SharePointサイトから複数のファイルを削除する + """ + try: + deleted_files = msgraphs_repo.sites.delete_multiple_files(site_id, file_names, folder_path) + return { + "message": f"{len(deleted_files)}個のファイルを削除しました", + "deleted_files": deleted_files + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"複数ファイルの削除に失敗しました: {str(e)}") \ No newline at end of file diff --git a/template_fastapi/settings/microsoft_graph.py b/template_fastapi/settings/microsoft_graph.py new file mode 100644 index 0000000..c7e3668 --- /dev/null +++ b/template_fastapi/settings/microsoft_graph.py @@ -0,0 +1,20 @@ +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + microsoft_graph_tenant_id: str = "" + microsoft_graph_client_id: str = "" + microsoft_graph_client_secret: str = "" + + model_config = SettingsConfigDict( + env_file=".env", + env_ignore_empty=True, + extra="ignore", + ) + + +@lru_cache +def get_microsoft_graph_settings() -> Settings: + return Settings() \ No newline at end of file