mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Add OneDrive file source
This commit is contained in:
@@ -32,6 +32,10 @@ export const templateTypes: FileSourceTypesDetail = {
|
||||
icon: faGoogleDrive,
|
||||
message: "This is a repository plugin that connects with the commercial Google Drive service.",
|
||||
},
|
||||
onedrive: {
|
||||
icon: faCloud,
|
||||
message: "This is a repository plugin that connects with Microsoft OneDrive through Microsoft Graph.",
|
||||
},
|
||||
onedata: {
|
||||
icon: faNetworkWired,
|
||||
message: "This is a repository plugin based on the Onedata service.",
|
||||
|
||||
@@ -12873,6 +12873,7 @@ export interface components {
|
||||
| "webdav"
|
||||
| "dropbox"
|
||||
| "googledrive"
|
||||
| "onedrive"
|
||||
| "elabftw"
|
||||
| "inveniordm"
|
||||
| "zenodo"
|
||||
@@ -24527,6 +24528,7 @@ export interface components {
|
||||
| "webdav"
|
||||
| "dropbox"
|
||||
| "googledrive"
|
||||
| "onedrive"
|
||||
| "elabftw"
|
||||
| "inveniordm"
|
||||
| "zenodo"
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
const OAUTH2_TYPES = ["dropbox", "googledrive"];
|
||||
const OAUTH2_TYPES = ["dropbox", "googledrive", "onedrive"];
|
||||
|
||||
const fileSourceTemplatesStore = useFileSourceTemplatesStore();
|
||||
fileSourceTemplatesStore.fetchTemplates();
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Annotated,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from pydantic import (
|
||||
AliasChoices,
|
||||
Field,
|
||||
)
|
||||
|
||||
from galaxy.exceptions import (
|
||||
AuthenticationRequired,
|
||||
MessageException,
|
||||
RequestParameterInvalidException,
|
||||
)
|
||||
from galaxy.files.models import (
|
||||
AnyRemoteEntry,
|
||||
BaseFileSourceConfiguration,
|
||||
BaseFileSourceTemplateConfiguration,
|
||||
Entry,
|
||||
EntryData,
|
||||
FilesSourceRuntimeContext,
|
||||
RemoteDirectory,
|
||||
RemoteFile,
|
||||
)
|
||||
from galaxy.util.config_templates import TemplateExpansion
|
||||
from . import BaseFilesSource
|
||||
|
||||
AccessTokenField = Field(
|
||||
...,
|
||||
title="Access Token",
|
||||
description="The OAuth2 access token for Microsoft Graph.",
|
||||
validation_alias=AliasChoices("oauth2_access_token", "accessToken", "access_token"),
|
||||
)
|
||||
|
||||
|
||||
class OneDriveFileSourceTemplateConfiguration(BaseFileSourceTemplateConfiguration):
|
||||
access_token: Annotated[Union[str, TemplateExpansion], AccessTokenField]
|
||||
drive_api_base: Union[str, TemplateExpansion] = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
|
||||
|
||||
class OneDriveFilesSourceConfiguration(BaseFileSourceConfiguration):
|
||||
access_token: Annotated[str, AccessTokenField]
|
||||
drive_api_base: str = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
|
||||
|
||||
class OneDriveFilesSource(
|
||||
BaseFilesSource[OneDriveFileSourceTemplateConfiguration, OneDriveFilesSourceConfiguration]
|
||||
):
|
||||
plugin_type = "onedrive"
|
||||
|
||||
template_config_class = OneDriveFileSourceTemplateConfiguration
|
||||
resolved_config_class = OneDriveFilesSourceConfiguration
|
||||
|
||||
def _headers(self, config: OneDriveFilesSourceConfiguration) -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"Bearer {config.access_token}",
|
||||
}
|
||||
|
||||
def _json_headers(self, config: OneDriveFilesSourceConfiguration) -> dict[str, str]:
|
||||
headers = self._headers(config)
|
||||
headers["Content-Type"] = "application/json"
|
||||
return headers
|
||||
|
||||
def _encoded_path(self, path: str) -> str:
|
||||
normalized = path.strip("/")
|
||||
if not normalized:
|
||||
return ""
|
||||
return "/".join(quote(component, safe="") for component in normalized.split("/"))
|
||||
|
||||
def _item_url(self, config: OneDriveFilesSourceConfiguration, path: str) -> str:
|
||||
api_base = config.drive_api_base.rstrip("/")
|
||||
encoded_path = self._encoded_path(path)
|
||||
if encoded_path:
|
||||
return f"{api_base}/special/approot:/{encoded_path}"
|
||||
return f"{api_base}/special/approot"
|
||||
|
||||
def _children_url(self, config: OneDriveFilesSourceConfiguration, path: str) -> str:
|
||||
return f"{self._item_url(config, path)}/children"
|
||||
|
||||
def _content_url(self, config: OneDriveFilesSourceConfiguration, path: str) -> str:
|
||||
return f"{self._item_url(config, path)}:/content" if path.strip("/") else f"{self._item_url(config, path)}/content"
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
context: FilesSourceRuntimeContext[OneDriveFilesSourceConfiguration],
|
||||
**kwargs,
|
||||
) -> requests.Response:
|
||||
try:
|
||||
response = requests.request(method, url, headers=self._headers(context.config), timeout=30, **kwargs)
|
||||
except requests.RequestException as exc:
|
||||
raise MessageException(f"Error connecting to OneDrive. Reason: {exc}") from exc
|
||||
|
||||
if response.status_code in {401, 403}:
|
||||
raise AuthenticationRequired(
|
||||
"Permission denied while accessing OneDrive. Check the Microsoft app registration, granted scopes, and the stored user authorization."
|
||||
)
|
||||
if response.status_code == 404:
|
||||
raise RequestParameterInvalidException(f"Path not found in OneDrive: {url}")
|
||||
if not response.ok:
|
||||
try:
|
||||
payload = response.json()
|
||||
message = payload.get("error", {}).get("message", response.text)
|
||||
except Exception:
|
||||
message = response.text
|
||||
raise MessageException(f"Error communicating with OneDrive. Reason: {message}")
|
||||
return response
|
||||
|
||||
def _entry_from_item(
|
||||
self,
|
||||
item: dict,
|
||||
parent_path: str,
|
||||
) -> AnyRemoteEntry:
|
||||
relative_parent = parent_path.rstrip("/")
|
||||
relative_path = f"{relative_parent}/{item['name']}".replace("//", "/")
|
||||
if not relative_path.startswith("/"):
|
||||
relative_path = f"/{relative_path}"
|
||||
uri = self.uri_from_path(relative_path)
|
||||
if "folder" in item:
|
||||
return RemoteDirectory(
|
||||
name=item["name"],
|
||||
uri=uri,
|
||||
path=relative_path,
|
||||
)
|
||||
return RemoteFile(
|
||||
name=item["name"],
|
||||
uri=uri,
|
||||
path=relative_path,
|
||||
size=item.get("size", 0),
|
||||
ctime=item.get("lastModifiedDateTime"),
|
||||
)
|
||||
|
||||
def _list(
|
||||
self,
|
||||
context: FilesSourceRuntimeContext[OneDriveFilesSourceConfiguration],
|
||||
path: str = "/",
|
||||
recursive: bool = False,
|
||||
write_intent: bool = False,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
query: Optional[str] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
) -> tuple[list[AnyRemoteEntry], int]:
|
||||
response = self._request("GET", self._children_url(context.config, path), context)
|
||||
items = response.json().get("value", [])
|
||||
entries = [self._entry_from_item(item, path) for item in items]
|
||||
return entries, len(entries)
|
||||
|
||||
def _realize_to(
|
||||
self, source_path: str, native_path: str, context: FilesSourceRuntimeContext[OneDriveFilesSourceConfiguration]
|
||||
):
|
||||
response = self._request("GET", self._content_url(context.config, source_path), context, stream=True)
|
||||
with open(native_path, "wb") as out:
|
||||
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
||||
if chunk:
|
||||
out.write(chunk)
|
||||
|
||||
def _write_from(
|
||||
self, target_path: str, native_path: str, context: FilesSourceRuntimeContext[OneDriveFilesSourceConfiguration]
|
||||
) -> str:
|
||||
upload_url = self._content_url(context.config, target_path)
|
||||
with open(native_path, "rb") as handle:
|
||||
response = requests.put(
|
||||
upload_url,
|
||||
headers={"Authorization": f"Bearer {context.config.access_token}", "Content-Type": "application/octet-stream"},
|
||||
data=handle,
|
||||
timeout=300,
|
||||
)
|
||||
if response.status_code in {401, 403}:
|
||||
raise AuthenticationRequired(
|
||||
"Permission denied while writing to OneDrive. Check the Microsoft app scopes and stored user authorization."
|
||||
)
|
||||
if not response.ok:
|
||||
try:
|
||||
payload = response.json()
|
||||
message = payload.get("error", {}).get("message", response.text)
|
||||
except Exception:
|
||||
message = response.text
|
||||
raise MessageException(f"Error uploading to OneDrive. Reason: {message}")
|
||||
return self.uri_from_path(target_path)
|
||||
|
||||
def _create_entry(
|
||||
self, entry_data: EntryData, context: FilesSourceRuntimeContext[OneDriveFilesSourceConfiguration]
|
||||
) -> Entry:
|
||||
parent_path = getattr(entry_data, "path", "/")
|
||||
payload = {
|
||||
"name": entry_data.name,
|
||||
"folder": {},
|
||||
"@microsoft.graph.conflictBehavior": "fail",
|
||||
}
|
||||
response = requests.post(
|
||||
self._children_url(context.config, parent_path),
|
||||
json=payload,
|
||||
headers=self._json_headers(context.config),
|
||||
timeout=30,
|
||||
)
|
||||
if response.status_code in {401, 403}:
|
||||
raise AuthenticationRequired(
|
||||
"Permission denied while creating a OneDrive folder. Check the Microsoft app scopes and stored user authorization."
|
||||
)
|
||||
if not response.ok:
|
||||
try:
|
||||
body = response.json()
|
||||
message = body.get("error", {}).get("message", response.text)
|
||||
except Exception:
|
||||
message = response.text
|
||||
raise MessageException(f"Error creating OneDrive folder. Reason: {message}")
|
||||
item = response.json()
|
||||
path = self._entry_from_item(item, parent_path).path
|
||||
return Entry(name=item["name"], uri=self.uri_from_path(path), external_link=item.get("webUrl"))
|
||||
|
||||
|
||||
__all__ = ("OneDriveFilesSource",)
|
||||
@@ -0,0 +1,15 @@
|
||||
- id: onedrive
|
||||
name: OneDrive
|
||||
description: Connect to your Microsoft OneDrive app folder to download and upload files.
|
||||
configuration:
|
||||
type: onedrive
|
||||
oauth2_client_id: "{{ environment.oauth2_client_id }}"
|
||||
oauth2_client_secret: "{{ environment.oauth2_client_secret }}"
|
||||
writable: true
|
||||
environment:
|
||||
oauth2_client_id:
|
||||
type: variable
|
||||
variable: GALAXY_ONEDRIVE_CLIENT_ID
|
||||
oauth2_client_secret:
|
||||
type: variable
|
||||
variable: GALAXY_ONEDRIVE_CLIENT_SECRET
|
||||
@@ -41,6 +41,7 @@ FileSourceTemplateType = Literal[
|
||||
"webdav",
|
||||
"dropbox",
|
||||
"googledrive",
|
||||
"onedrive",
|
||||
"elabftw",
|
||||
"inveniordm",
|
||||
"zenodo",
|
||||
@@ -113,6 +114,23 @@ class GoogleDriveFileSourceConfiguration(OAuth2FileSourceConfiguration, StrictMo
|
||||
oauth2_access_token: str
|
||||
|
||||
|
||||
class OneDriveFileSourceTemplateConfiguration(OAuth2TemplateConfiguration, StrictModel):
|
||||
type: Literal["onedrive"]
|
||||
writable: Union[bool, TemplateExpansion] = False
|
||||
oauth2_client_id: Union[str, TemplateExpansion]
|
||||
oauth2_client_secret: Union[str, TemplateExpansion]
|
||||
# Microsoft Graph app-folder scope keeps access limited to Apps/<Application Name>.
|
||||
oauth2_scope: Optional[Union[str, TemplateExpansion]] = None
|
||||
template_start: Optional[str] = None
|
||||
template_end: Optional[str] = None
|
||||
|
||||
|
||||
class OneDriveFileSourceConfiguration(OAuth2FileSourceConfiguration, StrictModel):
|
||||
type: Literal["onedrive"]
|
||||
writable: bool = False
|
||||
oauth2_access_token: str
|
||||
|
||||
|
||||
class S3FSFileSourceTemplateConfiguration(StrictModel):
|
||||
type: Literal["s3fs"]
|
||||
endpoint_url: Optional[Union[str, TemplateExpansion]] = None
|
||||
@@ -362,6 +380,7 @@ FileSourceTemplateConfiguration = Annotated[
|
||||
WebdavFileSourceTemplateConfiguration,
|
||||
DropboxFileSourceTemplateConfiguration,
|
||||
GoogleDriveFileSourceTemplateConfiguration,
|
||||
OneDriveFileSourceTemplateConfiguration,
|
||||
eLabFTWFileSourceTemplateConfiguration,
|
||||
InvenioFileSourceTemplateConfiguration,
|
||||
ZenodoFileSourceTemplateConfiguration,
|
||||
@@ -384,6 +403,7 @@ FileSourceConfiguration = Annotated[
|
||||
WebdavFileSourceConfiguration,
|
||||
DropboxFileSourceConfiguration,
|
||||
GoogleDriveFileSourceConfiguration,
|
||||
OneDriveFileSourceConfiguration,
|
||||
eLabFTWFileSourceConfiguration,
|
||||
InvenioFileSourceConfiguration,
|
||||
ZenodoFileSourceConfiguration,
|
||||
@@ -464,6 +484,7 @@ TypesToConfigurationClasses: dict[FileSourceTemplateType, type[FileSourceConfigu
|
||||
"webdav": WebdavFileSourceConfiguration,
|
||||
"dropbox": DropboxFileSourceConfiguration,
|
||||
"googledrive": GoogleDriveFileSourceConfiguration,
|
||||
"onedrive": OneDriveFileSourceConfiguration,
|
||||
"elabftw": eLabFTWFileSourceConfiguration,
|
||||
"inveniordm": InvenioFileSourceConfiguration,
|
||||
"zenodo": ZenodoFileSourceConfiguration,
|
||||
@@ -486,6 +507,12 @@ OAUTH2_CONFIGURED_SOURCES: ConfiguredOAuth2Sources = {
|
||||
token_url="https://oauth2.googleapis.com/token",
|
||||
scope="https://www.googleapis.com/auth/drive.file",
|
||||
),
|
||||
"onedrive": OAuth2Configuration(
|
||||
authorize_url="https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
token_url="https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
authorize_params={},
|
||||
scope="offline_access Files.ReadWrite.AppFolder",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user