diff --git a/StorageAddon/README.md b/StorageAddon/README.md index 23470c0..80b8846 100644 --- a/StorageAddon/README.md +++ b/StorageAddon/README.md @@ -1,711 +1,878 @@ # ストレージアドオンの作成 -具体的なストレージアドオンの例として、AWS S3互換の[MinIO](https://min.io/)と接続するストレージアドオンを実装します。このアドオンは特定のMinIOサーバーと接続するものとし、 My MinIOアドオンと名付けることとします。 +具体的なストレージアドオンの例として、[NextCloud](https://nextcloud.com/)と接続するストレージアドオンを実装します。このアドオンをNextCloudアドオンと名付け、`nextcloud_plugin`という独立したPythonパッケージとして実装します。 -基本的なアドオンの概要は[スケルトンの作成](../Skelton/README.md)を参照してください。 +本ドキュメントは以下の流れで構成されています。 -## 前提条件 +1. **[開発環境の準備](#開発環境の準備)**: osf.io・GravyValet・WaterButler・angular-osf からなる開発環境を、[Caddy](https://caddyserver.com/)による単一オリジンプロキシとともに起動します。 +2. **[ストレージアドオンの設計](#ストレージアドオンの設計)**: ストレージアドオンを構成する3要素(osf.io Addon / GravyValet Addon Imp / WaterButler Provider)とその実装方法を説明します。 +3. **[ストレージアドオンの利用方法](#ストレージアドオンの利用方法)**: 作成したアドオンを各サービスにインストール・設定します。 +4. **[NextCloud アドオンの動作確認](#nextcloud-アドオンの動作確認)**: NextCloud を接続先として、実際にファイル操作を試します。 -[開発環境の準備](../Environment.md#開発環境でRDMを起動する)のガイドに従い、開発環境にてRDMを起動しているものとします。 +本ガイドと合わせて、実装済みの参考例である [`nextcloud_plugin`](https://github.com/chiku-samugari/nextcloud_plugin) のソースコードを参照することを推奨します。 -# ストレージアドオンの設計 +# 開発環境の準備 -## サービスの構成 +ストレージアドオンの開発に先立って、開発環境の準備方法を説明します。開発環境は以下の5つの要素から構成されます。 -ストレージアドオンは以下の2つの要素から構成されます。 +- **osf.io**: バックエンドの中核。ユーザ情報を保持し、WebサーバとAPIサーバを提供します。`osf.io` ディレクトリの Compose プロジェクトとして起動します。 +- **GravyValet**: アドオンについての情報(接続先サービス・認証情報・設定)を管理します。`gravyvalet` ディレクトリの Compose プロジェクトとして起動します。 +- **WaterButler**: ストレージサービスへのファイルアクセスを担います。`waterbutler` ディレクトリのCompose プロジェクトとして起動します。 +- **angular-osf**: フロントエンド。`angular-osf` ディレクトリで `ng serve` により起動します。 +- **Caddy**: 上記4サービスを単一オリジン(`http://localhost`)に束ねるリバースプロキシ(Dockerコンテナとして起動)。 -- OSF.ioサービスで動作するAddon: ユーザ・プロジェクト設定の管理 -- WaterButlerサービスで動作するProvider: ストレージへのアクセスの仲介 +このように、バックエンドを [osf.io](https://github.com/CenterForOpenScience/osf.io)・[GravyValet](https://github.com/CenterForOpenScience/gravyvalet)・[WaterButler](https://github.com/CenterForOpenScience/waterbutler) が、フロントエンドを [angular-osf](https://github.com/CenterForOpenScience/angular-osf) が担います。 +[`RDM-osf.io` を中心とした従来の構成](../Environment.md)では、バックエンドとフロントエンド、すべてのサービスが1つの`docker-compose[.override].yml`(単一のComposeプロジェクト)にまとめられていましたが、本構成ではosf.io・GravyValet・WaterButler ・angular-osfはそれぞれ独立したリポジトリで扱い、それぞれのディレクトリで独立したDocker Composeプロジェクトとして起動します(angular-osfは`ng serve`(開発サーバ)で起動します)。 +ブラウザから見ると、これらは本来別々のオリジンで動作します。しかしosf.io の認証(セッションCookie、CASのログイン往復、同一オリジンへのリダイレクト制約)は、フロントエンドとバックエンドが単一のオリジンで提供されることを前提としています。そこで開発環境では、[Caddy](https://caddyserver.com/)による**単一オリジンのリバースプロキシ**を`http://localhost`に立て、アクセスを各サービスへ振り分けます。 -Addonによりユーザからの認証情報の受領や各種設定を行い、Providerはこの認証情報・設定情報をAddonから譲渡してもらい、実際のストレージへのアクセスを行います。 +![単一オリジンプロキシによるルーティング(ブラウザ → Caddy → 各サービス)](images/proxy-routing.png) -## ファイルの構成 +> **ブラウザ向けURLと、コンテナ間URLは別物です。** ブラウザ向けURLは `http://localhost`(Caddy 経由)に統一しますが、コンテナ間の通信(DB・CAS検証・サーバサイドのAPI/WB呼び出しなど)は、ループバックエイリアス `192.168.168.167`(osf.io のローカル開発の慣例)を用います。単一オリジンにするのは *ブラウザ向け* のみです。 -典型的なクラス構成とファイル配置は以下のようになります。 - -### OSF.io Addonのクラス構成とファイル構成 - -![OSF.io Addon クラス構成](images/osf_class.png) - -`(*)`が付いているファイルはスケルトン アドオンには存在しないファイルです。 - -``` -/addons/アドオン名/ -├── __init__.py ... モジュールの定義 -├── apps.py ... アプリケーションの定義 -├── models.py ... モデルの定義 -├── provider.py ... (*) 認証プロバイダの定義 -├── requirements.txt ... 利用するPythonモジュールの定義 -├── routes.py ... View(Routes)の定義 -├── serializer.py ... (*) モデル-ビュー(JavaScript)間の情報交換用シリアライザの定義 -├── settings ... 設定を定義するモジュール -│ ├── defaults.py ... デフォルト設定の定義 -│ ├── __init__.py ... 設定の定義 -│   └── local-dist.py ... (*) local.pyのサンプルファイル -├── static ... Webブラウザから読み込むことを想定した静的ファイル -│   ├── comicon.png ... アドオンのアイコン -│   ├── myminioAnonymousLogActionList.json ... (*) 変更履歴メッセージ定義 -│   ├── myminioLogActionList.json ... (*) 変更履歴メッセージ定義 -│   ├── myminioNodeConfig.js ... (*) Node設定の定義 -│   ├── myminioUserConfig.js ... (*) User設定の定義 -│   ├── node-cfg.js ... Node設定のエントリとなるJavaScriptファイル -│   └── user-cfg.js ... (*) User設定のエントリとなるJavaScriptファイル -├── templates ... テンプレートディレクトリ -│   ├── credentials_modal.mako ... (*) 認証情報の設定用ダイアログ -│   ├── node_settings.mako ... Node設定パネル -│   └── user_settings.mako ... (*) User設定パネル -├── tests ... テストコード -│   ├── __init__.py -│   ├── conftest.py -│   ├── factories.py -│   ├── test_model.py -│   ├── test_serializer.py ... (*) -│   ├── test_view.py -│   └── utils.py -├── utils.py ... (*) ユーティリティ関数の定義 -└── views.py ... View(Views)の定義 -``` - -### WaterButler Providerのクラス構成とファイル構成 - -![WaterButler Addon クラス構成](images/wb_class.png) - -``` -waterbutler/providers/アドオン名/ -├── __init__.py ... Providerクラスの参照 -├── metadata.py ... Metadataの定義 -├── provider.py ... Providerの定義 -└── settings.py ... デフォルト設定の定義 - -tests/providers/アドオン名/ -├── __init__.py -└── provider.py ... Providerのテストコード -``` +## 前提条件・共通の準備 -## OSF.io Addonのモジュール構成 +開発環境に以下のソフトウェアを準備します。 -スケルトン アドオンとの違いを中心に説明していきます。 +- **Docker**と**Docker Compose** +- **Node.js**と`npx` +- osf.io・GravyValet・WaterButler・angular-osf の各リポジトリを取得 +- **ループバックエイリアス `192.168.168.167`** を設定 -### Modelの構成 + ```bash + $ sudo ifconfig lo:0 192.168.168.167 netmask 255.255.255.255 up + ``` -`models.py` に、以下のModelを定義します。 +以降で追加する設定は、新規ユーザの登録に関するスイッチの既定値を指定するために編集する`features.yaml`以外はリポジトリにコミットされない**上書き用ファイル**(`docker-compose.override.yml`, `.docker-compose.local.env`, `config.json`, `Caddyfile` など)に記述します。追跡対象のファイルは upstream の値のまま変更しません。 -- `UserSettings`: ユーザーに関する情報(認証情報等) -- `NodeSettings`: プロジェクトに関する情報 -- `アドオン名FileNode`: ファイル・フォルダオブジェクトの親定義 -- `アドオン名File`: ファイルオブジェクトの定義 -- `アドオン名Folder`: ファイルオブジェクトの定義 +## osf.io の準備と起動 -`provider.py` に `アドオン名Provider` を、 `serializer.py` に `アドオン名Serializer` を定義します。OAuthを用いる場合は `osf.models.external.ExternalProvider` を継承し、必要なメンバを定義します。実装は [GitHubの例](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/github/models.py#L49) などを参考にしてください(GitHubアドオンは `アドオン名Provider` を `models.py` に定義しています)。 -`アドオン名Serializer` にはビュー(JavaScript)にURLリストや接続先フォルダなどを渡すための関数を定義します。 +`osf.io`ディレクトリで作業します。 -OAuth認証をしない場合であっても、ストレージアドオンの `UserSettings` と `NodeSettings` は、 `BaseOAuthUserSettings` と `BaseOAuthNodeSettings` をそれぞれ継承して定義し、 `oauth_provider` に`アドオン名Provider` を指定します。こうすることで、統一的な構造で簡単に認証の仕組みを実装することができます。 +### 設定の上書き -My MinIOアドオンの場合は、以下のように定義します。 +まずWebサーバとAPIサーバに関するDjangoの設定ファイル`local.py`を準備します。特に変更の必要がなければ、それぞれの`local-dist.py`をコピーして使用します。 +```bash +$ cp api/base/settings/local-dist.py api/base/settings/local.py +$ cp website/settings/local-dist.py website/settings/local.py +``` -- [models.py](osf.io/addon/models.py) -- [provider.py](osf.io/addon/provider.py) -- [serializer.py](osf.io/addon/serializer.py) +`web` / `api` / `worker`は`.docker-compose.env`を読み込みます。このファイルはリポジトリの保持する内容から変えず、上書き用のenvファイル`.docker-compose.local.env`を以下の内容で作成します。 -### Viewの構成 +```dotenv +DOMAIN=http://localhost/ +API_DOMAIN=http://localhost/ +WATERBUTLER_URL=http://localhost +``` -`views.py`には、おおよそ以下の関数を定義します。 +また、このファイルを`web` / `api` / `worker`が読み込むようにサービスの設定を上書きします。これも同様に、`osf.io/docker-compose.yml`を変更するのではなく、`osf.io/docker-compose.override.yml`を作ることで設定を上書きします。`osf.io/docker-compose.override.yml`を以下の内容で作成します。 -| 関数名 | 処理 | -|:------|:----| -| set_config | プロジェクトの設定を保存する。デフォルトでは接続先のフォルダとクライアント用のAPIのURLリストのみ。 | -| get_config | プロジェクトの設定を取得する。 | -| import_auth | ログイン中のユーザの認証情報をプロジェクトにインポートする。 | -| deauthorize_node | プロジェクトの認証情報を取り消す。 | -| add_user_account | ユーザの認証情報を追加する。 | -| account_list | ユーザの認証情報リストを取得する。 | -| create_folder | ストレージサービスにフォルダを追加する。プロジェクトのアドオン設定ページでフォルダを作る機能を提供しない場合は不要。 | -| folder_list | ストレージサービスのフォルダリストを取得する。 | +```yaml +services: + web: + env_file: [.docker-compose.env, .docker-compose.local.env] + api: + env_file: [.docker-compose.env, .docker-compose.local.env] + worker: + env_file: [.docker-compose.env, .docker-compose.local.env] +``` -シリアライザ(`serializer.py`)を定義し、 `addons.base.generic_views` を利用することで、以下のように一部のView処理を簡単に定義することができます。 +### ライブラリのインストールとMigration +ライブラリのインストールとMigrationを実行するコマンドを実行します。1つ目については依存するライブラリが変更されたとき、2つ目についてはモデルなどのDB定義の定義に変更があった際に実行する必要があります。サービスの起動毎に毎回実行する必要があるものではありません。 + +```bash +# ライブラリのインストール(初回/依存定義の変更時) +$ docker compose up requirements + +# DBのMigration(初回/DB定義の変更時) +$ docker compose run --rm web python3 manage.py migrate ``` -import_auth = addons.base.generic_views.import_auth( - SHORT_NAME, - Serializer -) -``` -`generic_views` で提供していないView処理を追加したい場合や、View処理をカスタマイズしたい場合は、以下のように `views.py` に個別の関数を定義します。 +### サービスの起動 + +以下のコマンドでストレージサービスアドオンの開発に必要なサービスを起動します。 +```bash +$ docker compose up -d assets fakecas worker web api ``` -@must_have_addon(SHORT_NAME, 'node') -@must_be_addon_authorizer(SHORT_NAME) -def folder_list(node_addon, **kwargs): - return node_addon.get_folders() + +新規ユーザの登録を行なう場合は `mailhog` が必要な場合があるので、これを追加して起動します。これについて詳細は[「新規ユーザの登録とメール確認」](#新規ユーザの登録とメール確認)を参照してください。。 + +```bash +$ docker compose up -d assets fakecas worker web api mailhog ``` -フォルダの作成やフォルダリストの取得をするために、Addonでもストレージサービスへ接続する必要があります。 +また、管理者機能が必要な場合には、`admin`と`admin_assets`を追加して起動してください。 -My MinIOアドオンの場合は、以下のように定義します。 +```bash +$ docker compose up -d assets fakecas worker web api admin_assets admin +``` -- [routes.py](osf.io/addon/routes.py) -- [views.py](osf.io/addon/views.py) +## GravyValet の準備と起動 +`gravyvalet` ディレクトリで作業します。まず以下の内容で`docker-compose.override.yml`を作成します。 -### フレームワークによって提供されるView +```yaml +services: + gravyvalet: + environment: + OSF_BASE_URL: "http://localhost" + celeryworker: + environment: + OSF_BASE_URL: "http://localhost" + celerybeat: + environment: + OSF_BASE_URL: "http://localhost" + postgres: + image: postgres:17 +``` -アドオンが持つ利用者用設定画面(`user_settings.mako`)とプロジェクト用設定画面(`node_settings.mako`)のテンプレートをそれぞれ定義します。認証情報の設定用ダイアログ(`credentials_modal.mako`)はどちらの画面でも利用するので、別のファイルで定義し、それぞれから参照します。 -My MinIOアドオンの場合は、以下のように定義します。 +GravyValetには`local.py`のフックがなく、環境変数がフックです。環境変数`OSF_BASE_URL`から、受理するリソースURIの接頭辞(`ALLOWED_RESOURCE_URI_PREFIXES`)を導出します。プロキシのオリジンを指定し、プロジェクト(`http://localhost/`)のリソースを認識させます。一方、`OSF_API_BASE_URL`はそのままにします。こちらはGravyValetがosf.io API を呼ぶ際に`192.168.168.167`エイリアス経由で使う値であり、ブラウザ向けではありません。 -- [user_settings.mako](osf.io/addon/templates/user_settings.mako) -- [node_settings.mako](osf.io/addon/templates/node_settings.mako) -- [credentials_modal.mako](osf.io/addon/templates/credentials_modal.mako) +また、`docker-compose.yml`では`postgres`サービスのイメージとして`postgres:latest`を指定しており、起動できないケースが確認されています。`postgres:17`を指定しているのはそのための対処です。 -利用者用設定画面のJavaScriptファイル(`user-cfg.js`, `myminioUserConfig.js`)と、プロジェクト用設定画面のJavaScriptファイル(`node-cfg.js`, `myminioNodeConfig.js`)をそれぞれ定義します。今回は、エントリとなるJavaScriptファイル(`*-cfg.js`)と定義ファイル(`myminio*Config.js`)を分けましたが、スケルトン アドオンのように `*-cfg.js` に定義を書いても構いません。 -My MinIOアドオンの場合は、以下のように定義します。 +続いて以下のコマンドで初期化を実施します。 -- [user-cfg.js](osf.io/addon/static/user-cfg.js) -- [node-cfg.js](osf.io/addon/static/node-cfg.js) -- [myminioUserConfig.js](osf.io/addon/static/myminioUserConfig.js) -- [myminioNodeConfig.js](osf.io/addon/static/myminioNodeConfig.js) +```bash +$ docker compose up -d --build +$ docker compose exec gravyvalet python manage.py migrate +$ docker compose exec gravyvalet python manage.py fill_external_services +$ docker compose exec gravyvalet python manage.py createsuperuser +``` -また、変更履歴メッセージの定義ファイルも追加します。 -My MinIOアドオンの場合は、以下のように定義します。 +`migrate`はMigrationの実行で、初回以外にもモデルの追加や変更があったときに実行します。`fill_external_services`はGravyValetのコードベースに組み込まれているアドオン実装からExternalServiceを一通り作るコマンドで、組み込まれているストレージアドオン(S3やGoogleDriveなど)を利用することができるようになりますが、必須ではありません。`createsuperuser`によりGravyValetの管理者を作成し、`localhost:8004/admin`にブラウザでアクセスしてExternalServiceを追加することもできます。本ドキュメントで作成するアドオンからもExternalServiceを作る必要があるので、この管理者を作ることは必須です。普段の起動は`docker compose up -d`のみで十分です。 -- [myminioAnonymousLogActionList.json](osf.io/addon/static/myminioAnonymousLogActionList.json) -- [myminioLogActionList.json](osf.io/addon/static/myminioLogActionList.json) +### 補足 -ストレージ操作UIであるFileViewerは[Fangorn](https://github.com/RCOSDP/RDM-osf.io/blob/develop/website/static/js/fangorn.js)を使って実装されています。アイテム選択時に表示するボタンをカスタマイズしたい場合は、 `Fangorn.config.アドオン名` を定義し、 `files.js` ファイルで読み込みます。 -My MinIOアドオンではカスタマイズせずデフォルト動作を使用しています。カスタマイズ例は、[GitHubアドオン](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/github/static/githubFangornConfig.js)や[IQB-RIMSアドオン](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/iqbrims/static/iqbrimsFangornConfig.js)を参照してください。 +- GravyValetのコンテナは`gravyvalet`という名前です。例えば、GravyValetのログは以下のようにして確認することができます。 + ```bash + $ docker compose logs -f waterbutler + ``` -### 設定モジュール +- Postgresは`postgres`コンテナで動作しており、DBを直接見たい場合はユーザを`postgres`、DBとして`gravyvalet`を指定します。 + ```bash + $ docker compose exec postgres psql -U postgres -W gravyvalet + ``` -環境ごとの設定ファイル `local.py` の雛形として `local-dist.py` ファイルを定義します。サービス管理者は `local-dist.py` を `local.py` にコピーして、適宜設定値を書き換えます。 +## WaterButler の準備と起動 -My MinIOアドオンの場合は、以下のように定義します。接続するMinIOサービスのホスト名を `HOST` プロパティに設定します。 +`waterbutler` ディレクトリで作業します。WaterButlerリポジトリには`docker-compose.yml`が含まれていません。以下の内容の`docker-compose.yml`を作成します。 -- [local-dist.py](osf.io/addon/settings/local-dist.py) +```yaml +services: + waterbutler: + build: + context: . + command: > + bash -c " + gosu www-data /code/.venv/bin/python -m invoke server + " + restart: unless-stopped + env_file: + - .docker-compose.env + ports: + - 7777:7777 + volumes: + - ./:/code:cached + - /code/.venv + stdin_open: true + depends_on: + - celery -### テストコード + celery: + build: + context: . + command: + gosu www-data /code/.venv/bin/python -m invoke celery + restart: unless-stopped + environment: + C_FORCE_ROOT: 1 + env_file: + - .docker-compose.env + stdin_open: true +``` -シリアライザのテスト(`test_serializer.py`)を定義します。 -My MinIOアドオンの場合は、以下のように定義します。 +続いて、以下のコマンドで起動します。 -- [test_serializer.py](osf.io/addon/tests/test_serializer.py) +```bash +$ docker compose up -d --build +``` -## WaterButler Providerのモジュール構成 +### 補足 -WaterButler Providerは、WaterButlerプロジェクトの中でPythonモジュールとして実装されます。 +- WaterButlerのコンテナは`waterbutler`という名前です。例えばWaterButlerのログは以下のコマンドで確認することができます。 + ```bash + $ docker compose logs -f waterbutler + ``` -`metadata.py` には、フォルダやファイルのメタデータと、リビジョンを持つクラスを定義します。 -My MinIOアドオンの場合は、以下のように定義します。 +## angular-osf の準備と起動 -- [metadata.py](waterbutler/provider/metadata.py) +`angular-osf` ディレクトリで作業します。まず依存パッケージをインストールします。これは初回だけ実行します。 -`provider.py`には、ストレージサービスと接続しCRUD操作などを行うProviderクラスを定義します。 +```bash +$ npm install +``` -Providerが提供するメソッドには以下のようなものがあります。 +次に`src/assets/config/config.json`を以下の内容で作成します。このファイルは`.gitignore`によりgit管理から外されている設定ファイルです。これがないとangular-osfはステージング環境を指します。 +```json +{ + "webUrl": "http://localhost", + "apiDomainUrl": "http://localhost", + "addonsApiUrl": "http://localhost/v1", + "casUrl": "http://localhost:8080", + "recaptchaSiteKey": "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI", + "sentryDsn": "", + "googleTagManagerId": "", + "newRelicEnabled": false +} +``` -| 関数名 | 引数 | 戻り値 | 処理 | -|:------|:----|:------|:-----| -| validate_v1_path | path, **kwargs | WaterButlerPath | 文字列で与えられたパス情報(`path`)を検証し、属性付きのWaterButlerPathオブジェクトを返す。 | -| validate_path | path, **kwargs | WaterButlerPath | 同上(廃止予定のv0仕様との互換性維持のため、2つのメソッドに分かれている)。 | -| download | path, accept_url=False, version=None, range=None, **kwargs | Stream | 指定されたパス(`path`)のデータをダウンロードする。戻り値にはデータアクセス用のStreamを返す。 | -| upload | stream, path, conflict='replace', **kwargs | Metadata | 指定されたパス(`path`)に指定されたデータ(`stream`)をアップロードする。戻り値にはアップロードしたファイルを示すMetadataを返す。 | -| delete | path, confirm_delete=0, **kwargs | なし | 指定されたパス(`path`)のファイル・フォルダを削除する。 | -| revisions | path, **kwargs | List(Revision) | 指定されたパス(`path`)のリビジョン情報を取得する。 | -| metadata | path, revision=None, **kwargs | Metadata or List(Metadata) | 指定されたパス(`path`)のメタデータを取得する。`path` がfileの場合 `Metadata` , directoryの場合 `List(Metadata)` を返す。 | -| create_folder | path, folder_precheck=True, **kwargs | Metadata | 指定されたパスにフォルダを作成する。戻り値には作成したフォルダを示すMetadataを返す。 | -| can_intra_copy | dest_provider, path=None | Bool | 指定された送信先Provider(`dest_provider`), パス(`path`)に対してintra_copy(内部コピー: ストレージサービス上でのコピー)が可能かどうかを判定する。これがFalseの場合、いったん一時ディレクトリにdownloadして、destにuploadするという操作となる。 | -| can_intra_move | dest_provider, path=None | Bool | 指定された送信先Provider(`dest_provider`), パス(`path`)に対してintra_move(内部移動: ストレージサービス中での移動)が可能かどうかを判定する。これがFalseの場合、いったん一時ディレクトリにdownloadして、destにupload、コピー元ファイルをdeleteするという操作となる。 | -| intra_copy | dest_provider, src_path, dest_path | Bool | 内部コピーを実施する。成功すればTrueを返す。 | -| intra_move | dest_provider, src_path, dest_path | Bool | 内部移動を実施する。成功すればTrueを返す。 | +開発サーバは **`development`** 構成で起動します(i.e. `src/environments/environment.development.ts`)。 -My MinIOアドオンの場合は、以下のように定義します。 +```bash +$ npx ng serve --configuration development --host 0.0.0.0 --port 4200 --poll 2000 +``` -- [provider.py](waterbutler/provider/provider.py) +## Caddyを用いた単一オリジンプロキシの起動 +任意の場所に `Caddyfile`というファイルを以下の内容で作成します。 -## OSF.io AddonとWaterButler Providerの認証情報の委譲 +```caddyfile +{ + admin off + auto_https off +} -OSF.io Addonは、WaterButler Providerが必要なサービスに接続できるよう、認証情報を委譲します。この認証情報の形式はAddonにより異なるため、AddonとProviderのバージョンを合わせるなど依存関係に配慮する必要があります。 +http://localhost { + # osf.io API (Django, :8000) + @api path /v2 /v2/* /_/* + handle @api { reverse_proxy 127.0.0.1:8000 } -![認証情報などの委譲](images/crud.png) + # WaterButler (ファイル操作) — /v1 より前に置くこと(GravyValet は /v1/resource-references を使う) + @wb path /v1/resources/* + handle @wb { reverse_proxy 127.0.0.1:7777 } -Addonにおける委譲設定は、 `addons.アドオン名.models.NodeSettings.serialize_waterbutler_credentials()` 関数で設定します。接続先のフォルダなどの情報は、同クラスの `serialize_waterbutler_settings()` 関数で設定します。例えば [IQB-RIMSアドオンの `serialize_waterbutler_settings()` 関数](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/iqbrims/models.py#L226) では、フォルダごとの権限設定を渡します。 + # GravyValet (アドオンサービス, Django, :8004) + @gv path /v1 /v1/* + handle @gv { reverse_proxy 127.0.0.1:8004 } -My MinIOアドオンの場合は、以下のように定義します。認証情報として、アクセス先のホスト名、アクセスキー、シークレットキーを渡します。設定情報として、接続先のバケットIDを渡します。 + # GravyValet のアドオンアイコン + @gvicons path /static/provider_icons/* /static/*/icons/* + handle @gvicons { reverse_proxy 127.0.0.1:8004 } -``` -def serialize_waterbutler_credentials(self): - if not self.has_auth: - raise exceptions.AddonError('Cannot serialize credentials for {} addon'.format(FULL_NAME)) - return { - 'host': settings.HOST, - 'access_key': self.external_account.oauth_key, - 'secret_key': self.external_account.oauth_secret, - } + # osf.io web + CAS (Flask, :5000) — /confirm は新規ユーザのメール確認リンク(後述) + @flask path /login /login/* /logout /logout/* /oauth /oauth/* /api/v1 /api/v1/* /download /download/* /confirm /confirm/* + handle @flask { reverse_proxy 127.0.0.1:5000 } -def serialize_waterbutler_settings(self): - if not self.folder_id: - raise exceptions.AddonError('Cannot serialize settings for {} addon'.format(FULL_NAME)) - return { - 'bucket': self.folder_id - } + # angular-osf の開発サーバ (:4200) — Host は "localhost" のまま(localhost:4200 に書き換えない) + handle { reverse_proxy 127.0.0.1:4200 } +} ``` -Provider側では、認証情報と設定情報を `waterbutler.providers.アドオン名.アドオン名Provider` クラスのコンストラクタ(`__init__()`)の引数 `credentials` と `settings` にDictionary型でそれぞれ受け取ります。 - -My MinIOアドオンの場合は、以下のように定義します。認証情報を使ってMy MinIOサービスとの接続を確立し、設定情報を使って接続先バケットを取得します。 +このファイルが配置されたディレクトリで以下のコマンドを実行することで起動します。 +```bash +$ docker run -d --name osf-proxy --restart unless-stopped --network host -v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2 ``` -class MyMinIOProvider(provider.BaseProvider): - def __init__(self, auth, credentials, settings): - super().__init__(auth, credentials, settings) - host = credentials['host'] - port = 443 - m = re.match(r'^(.+)\:([0-9]+)$', host) - if m is not None: - host = m.group(1) - port = int(m.group(2)) - self.connection = MyMinIOConnection(credentials['access_key'], - credentials['secret_key'], - calling_format=OrdinaryCallingFormat(), - host=host, - port=port, - is_secure=port == 443) - self.bucket = self.connection.get_bucket(settings['bucket'], validate=False) -``` +## 起動手順のまとめ -## Recent Activityの記録・表示 +ここまでに述べた内容は、開発環境を初めて構成するときに必要な初期化を含んでおり、手順の多くは初めて構成するときにのみ必要なものです。そのような手順を省くと以下の通りになります。初回以外はこの手順により開発環境を立ち上げることができます。 -何らかのユーザ操作を契機としてアドオンに対して行われた操作は、Recent Activityという形で記録することができます。 +```bash +# 0) ループバックエイリアス +$ sudo ifconfig lo:0 192.168.168.167 netmask 255.255.255.255 up -### NodeLogモデルの追加 +# 1) バックエンド +$ cd /path/to/osf.io && docker compose up -d assets fakecas worker web api # mailhog, admin, admin_assetsは必要に応じて追加 +$ cd /path/to/gravyvalet && docker compose up -d +$ cd /path/to/waterbutler && docker compose up -d -Recent Activityは[NodeLogモデル](https://github.com/RCOSDP/RDM-osf.io/blob/develop/osf/models/nodelog.py)により表現されます。 -ログの追加はNode(プロジェクトに対応するモデル)の [add_logメソッド](https://github.com/RCOSDP/RDM-osf.io/blob/develop/osf/models/mixins.py#L84) により行うことができます。 +# 2) angular-osf の開発サーバ(別ターミナルで起動、コンパイルに十数秒かかる) +$ cd /path/to/angular-osf +$ npx ng serve --configuration development --host 0.0.0.0 --port 4200 --poll 2000 -[models.py](osf.io/addon/models.py#L127-L143) +# 3) プロキシ +$ cd /path/to/Caddyfile +$ docker start osf-proxy # 初回は「単一オリジンプロキシ」の docker run を使用 ``` -self.owner.add_log( - '{0}_{1}'.format(SHORT_NAME, action), - auth=auth, - params={ - 'project': self.owner.parent_id, - 'node': self.owner._id, - 'path': metadata['materialized'], - 'bucket': self.folder_id, - 'urls': { - 'view': url, - 'download': url + '?action=download' - } - }, -) + +## Web UIを開く・動作確認 + +ブラウザで **http://localhost/** を開きます。osf.io のホームページが表示され、`Sign in` から fakecas を経由してサインインできます。ダッシュボード・プロジェクト・ファイルの参照まで、すべて単一の `http://localhost` オリジンで提供されます。 + +簡易的なヘルスチェック: + +```bash +$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost/ # 200 (angular-osf) +$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost/v2/ # 200 (API) ``` -この例では、NodeSettingsモデルのowner(Node)に対してログの追加を指示しています。 -パラメータには以下の値を指定することができます。 +## 新規ユーザの登録とメール確認 + +以前からosf.ioを利用してローカルに開発環境を整備していて、既にユーザを登録済みの場合、そのような既存ユーザは fakecas 経由で引き続きサインインできます。しかし、現在のosf.ioで **新規にユーザを登録** するには、確認メールの仕組みに追加の準備が必要です。osf.io の通知(Notification)基盤が刷新され確認メールの送信経路が変わったため、ローカル環境では以下の3点を揃える必要があります。 -- `action` ... ログのアクション種別を示す名前。`アドオン名_アクション名`となる。本サンプルにより記録される`アクション名`には以下のものがある。 - - `node_authorized`, `node_deauthorized`, `node_deauthorized_no_user` ... プロジェクトに本アドオンが設定あるいは解除された場合に記録されるログ - - `bucket_linked`, `bucket_unlinked` ... プロジェクト設定画面により、バケットが設定あるいは解除された場合に記録されるログ - - `file_added`, `file_removed`, `file_updated`, `folder_created` ... WaterButlerによるファイル操作が行われた場合に記録されるログ -- `params` ... ログのパラメータ。任意のdictオブジェクトを指定することができる -- `auth` ... 操作を実施したユーザの情報。[framework.auth.Authクラス](https://github.com/RCOSDP/RDM-osf.io/blob/develop/framework/auth/core.py#L170)のインスタンスを与えることができる +### MailHog と `enable_mailhog` スイッチ -### NodeLogモデルの表示 +確認メールは ローカル用のダミーSMTPサーバであるMailHog で受け取ります。加えて `enable_mailhog` の waffle スイッチを有効化します。これが無効(既定)のままだと送信は SendGrid 経路となり、ローカルにはAPIキーがないため送信時に例外が発生します。この例外により登録リクエストのトランザクションがロールバックされ、 **未確認ユーザ自体が作成されません** 。 -記録されたログをどのように表示するかは、以下のJSONファイルにより定義します。 +`osf/features.yaml` の該当スイッチ(`enable_mailhog`)を `active: true` に変更し、以下で反映します。 -[myminioLogActionList.json](osf.io/addon/static/myminioLogActionList.json#L2) +```bash +$ docker compose run --rm web python3 manage.py manage_switch_flags ``` -"myminio_bucket_linked" : "${user} linked the My MinIO bucket ${bucket} to ${node}", + +> `manage.py waffle_switch enable_mailhog on` でも一時的に有効化できますが、`migrate` のたびに `features.yaml` の値(既定は無効)にリセットされます。 + +以上を実施した後、ブラウザで`http://localhost`にアクセスして "Sign Up" ボタンから新規ユーザの登録を実行できます。ただし、登録確認のためにアクセスするURLは `web` コンテナのログから探し出してアクセスする必要があります。`web` コンテナのログから最新の確認URLだけを取り出すには次のようにします。 + +```bash +$ docker compose logs web | grep -oE 'http://localhost/confirm/[A-Za-z0-9]+/[A-Za-z0-9]+/' | tail -1 ``` -[myminioAnonymousLogActionList.json](osf.io/addon/static/myminioAnonymousLogActionList.json#L2) +一般的なユーザ登録の流れでは、ユーザが登録時に提出したメールアドレスにこの確認用URLを含むメールが送信され、ユーザはこのURLを入手することになります。このためのメール本文を生成し、メールを確認するためには[次節](#通知テンプレートの投入)を実施してください。 + +### 通知テンプレートの投入 + +ユーザ登録確認メールの件名と本文は DBの `NotificationType` レコードのテンプレートから生成されますが、新規のDBではテンプレートが空のため、 **本文が空・件名が `None`** のメールになります。以下のコマンドで、`notifications.yaml` と各テンプレートファイルから内容をDBに投入します。 + +```bash +$ docker compose run --rm web python3 manage.py populate_notification_types ``` -"myminio_bucket_linked" : "A user linked an My MinIO bucket to a project", + +> **2時間キャッシュに注意。** `NotificationType` はアプリのプロセス内で最大2時間キャッシュされます(`ttl_cached_property`)。確認メールは web プロセス内で同期的に送信されるため、テンプレート投入前に一度でも確認メールを送っていると、投入後もしばらく空メールが送られ続けます。投入後はアプリのコンテナを再起動してキャッシュを破棄してください(初回登録より前に投入を済ませていれば再起動は不要です)。 + +```bash +$ docker compose restart web api worker # osf.io ディレクトリで実行 ``` -`アドオン名LogActionList.json`はログインした状態でのプロジェクト表示の際に利用され、`アドオン名AnonymousLogActionList.json`はパブリックなプロジェクト(RDMでは利用を想定していません)に利用されます。 -どのメッセージがログ表示に利用されるかは、add_logメソッドの `action` 引数に与えられた文字列がキーとして使用されます。また、メッセージ定義中の `${パラメータ名}` にはadd_logメソッドの `params` 引数に与えられたパラメータ中のキーを指定することができます。 +> `features.yaml` の `populate_notification_types` スイッチは `migrate` 時の**自動投入**(`post_migrate` フック)を制御するもので、上記のようにコマンドを直接実行する場合は変更不要です。投入されるのは `NotificationType` の **データ(行)** であり、waffle スイッチの状態とは異なり `migrate` で消えることはありません(一度投入すれば維持されます)。一方、前掲の `enable_mailhog` は送信のたびに参照される実行時スイッチであり、`migrate` で `features.yaml` の値に戻ります。したがって、`migrate `のたびに`enable_mailhog` を `true` にする手順を実施するか、`features.yaml` を変更して既定値を `true` にしておくことが必要になります。 -メッセージの国際化は[pybabelコマンド](http://babel.pocoo.org/en/latest/cmdline.html)を用いて行うことができます。定義したアドオンのメッセージ(英語で記載する)に対応する日本語メッセージの定義ファイルを生成するためには、 -以下のコマンドを実行します。 +### Caddy の `/confirm` ルート +確認メールのリンク `http://localhost/confirm///` は osf.io(Flask, :5000)が処理するルートです。「[Caddyを用いた単一オリジンプロキシの起動](#caddyを用いた単一オリジンプロキシの起動)」の `Caddyfile` では、この `/confirm` を `@flask` に含めています。含めないと angular-osf(SPA)側に振り分けられ、`/v2/guids/confirm/` として解決されようとして 404 になります。 + +### 動作確認 + +1. ブラウザで **http://localhost/** を開き、サインアップします。 +2. MailHogのWeb UI **http://localhost:8025** を開くと、件名 **「OSF Account Verification」** のメールが届いています。本文には確認URLが含まれているので、そのURLを開きます。 +3. Flask がメール確認を完了し、CAS(検証キー)によるログインを経てダッシュボードに遷移すれば成功です。 + +確認URLは、MailHog の Web UI のほか、`mailhog` コンテナのログからも取得できます(ログを追う運用に慣れている場合に便利です)。MailHog は受信したメール本文をログに出力するため、「[通知テンプレートの投入](#通知テンプレートの投入)」を済ませて本文が生成されていれば、以下のように最新の確認URLを取り出せます。 + +```bash +$ docker compose logs mailhog | grep -oE 'http://localhost/confirm/[A-Za-z0-9]+/[A-Za-z0-9]+/' | tail -1 ``` -# メッセージ定義JSONなどをJavaScriptファイルへと変換する -$ docker compose run --rm web invoke assets -# メッセージ定義テンプレートファイル website/translations/js_messages.pot を更新する -$ docker compose run --rm web pybabel extract -F ./website/settings/babel_js.cfg -o ./website/translations/js_messages.pot . +以上で開発環境の準備は完了です。 + +# ストレージアドオンの設計 + +## ストレージアドオンの構成 + +1つのストレージサービスを扱うために以下の**3要素**を実装します。 + +- **osf.io Addon**: osf.io上で動作するDjangoアプリケーション。ファイルとフォルダのモデルを持ち、アドオンの登録を担います。 +- **GravyValet Addon Imp**: GravyValet上で動作するDjangoアプリケーション。フォルダ内容の一覧やWaterButler向け設定の生成を担います。 +- **WaterButler Provider**: WaterButler上で動作する実装。実際のストレージへのアクセス、すなわちファイルの取得や移動・名称変更・削除といった処理を担います。 + +これら3要素を1つの独立したPythonパッケージにまとめ、各サービス(osf.io / GravyValet / WaterButler)へ`pip` / `poetry`でインストールして利用します。`osf.io` / `GravyValet` / `WaterButler`のソースコード内にアドオンを埋め込む必要はありません。実装済みの参考例としてNextCloudと接続するストレージアドオンである[`nextcloud_plugin`](https://github.com/chiku-samugari/nextcloud_plugin)パッケージとS3互換ストレージと接続するストレージアドオン[`s3compat_plugin`](https://github.com/chiku-samugari/s3compat_plugin)パッケージがあります。 + +> GravyValet導入前は「osf.io Addon」と「WaterButler Provider」の2要素でしたが、GravyValetの導入により「Addon Imp」が加わりました。Addon Impは通常はGravyValet本体に組み込まれますが、本ガイドではGravyValet本体を変更せずに動的にAddon Impを追加できる**Foreign Addon Imp**の仕組みを利用します。 + +![ストレージアドオン構成(1つのパッケージを3サービスへインストール)](images/architecture.png) + +## ファイルの構成 + +ストレージアドオンの典型的なレイアウトは以下のようになります。`nextcloud_plugin` を例としています。 -# メッセージ定義ファイル website/translations/ja/LC_MESSAGES/js_messages.po を更新する -$ docker compose run --rm web pybabel update -i ./website/translations/js_messages.pot -o ./website/translations/en/LC_MESSAGES/js_messages.po -l en -$ docker compose run --rm web pybabel update -i ./website/translations/js_messages.pot -o ./website/translations/ja/LC_MESSAGES/js_messages.po -l ja +``` +nextcloud_plugin/ +├── pyproject.toml ... パッケージ定義、依存関係の宣言、WaterButlerエントリポイントの定義 +├── README.md +└── src/nextcloud_plugin/ + ├── addon/ ... osf.io Addonパッケージ + ├── addon_imp/ ... GravyValet Addon Impパッケージ + └── provider/ ... WaterButler Providerパッケージ ``` -すると、`website/translations/ja/LC_MESSAGES/js_messages.po`ファイルに、以下のような空の項目が追加されます。 +### osf.io Addon のファイル構成 ``` -#: website/static/js/logActionsList_extract.js:246 -msgid "${user} linked the My MinIO bucket ${bucket} to ${node}" -msgstr "" +src/nextcloud_plugin/addon/ +├── __init__.py ... パッケージ初期化ファイル +├── apps.py ... アプリケーションの定義 +├── models.py ... FileNodeモデル3クラスと`UserSettings`, `NodeSettings`の定義 +├── provider.py ... 認証プロバイダの定義 +├── serializer.py ... Node/User設定をJSON化するシリアライザの定義 +├── routes.py / views.py ... View(Routes/Views)の定義 +├── settings/ ... 設定モジュール +│ ├── __init__.py +│ └── defaults.py ... デフォルト設定(ホストの Django settings から`getattr`で上書き可能) +├── typedmodel_workaround.py ... `TypedModelRejoinMixin`を提供 +└── migrations/ ... このaddonのMigration(パッケージに同梱する) ``` -この `msgstr` に日本語によるメッセージ定義を追加することで、メッセージを国際化することができます。 +> ストレージアドオンではフロントエンドを提供しません。接続・設定・フォルダ選択・ファイルブラウズのUIは`angular-osf`と`GravyValet`が提供します。従来必要だったアドオンごとのmakoテンプレート(`node_settings.mako`等)、`*-cfg.js` / `*Config.js`、Fangornのカスタマイズ、ログ表示用JSON(`*LogActionList.json`)、`storageAddons.json`への登録、JavaScriptメッセージの国際化(pybabel)などは不要です。 + +### GravyValet Addon Impのファイル構成 ``` -#: website/static/js/logActionsList_extract.js:246 -msgid "${user} linked the My MinIO bucket ${bucket} to ${node}" -msgstr "${user}が My MinIOバケット(${bucket})を接続しました" +src/nextcloud_plugin/addon_imp/ +├── __init__.py +├── apps.py ... アプリケーションの定義 +├── imp.py ... AddonImpの実装 +└── static/{AppConfig.name}/icons/ ... アドオンのアイコンを配置するディレクトリ ``` -`js_messages.po` を変更したら、`assets`サービスを再起動してください。最新の`js_messages.po`ファイルがメッセージの表示に使用されるようになります。 +### WaterButler Providerのファイル構成 ``` -$ docker compose restart assets +src/nextcloud_plugin/provider/ +├── __init__.py +├── provider.py ... Provider の定義 +├── metadata.py ... Metadata の定義 +├── settings.py ... デフォルト設定の定義 +└── utils.py ... (必要に応じて)ユーティリティ ``` -## タイムスタンプの処理 +## 識別名について -RDMではユーザが任意のタイミングで、プロジェクト中のファイルに対してタイムスタンプを打つことができます。タイムスタンプは、ファイルに関してその時点での内容を証明するもので、研究の証跡としてのデータを考える上で非常に重要です。 +ストレージアドオンでは1つのストレージサービスを表す**1つの識別名(例: `nextcloud`)**を3要素で一貫して使う必要があります。これらが一致していない場合、ファイル操作時にAddonやProviderの解決に失敗します。 -なお、docker-composeで起動した状態では [FreeTSA Project](http://eswg.jnsa.org/sandbox/freetsa/) のサーバを用いてタイムスタンプを付与します。実環境への配備時は[UPKI電子証明書発行サービス](https://certs.nii.ac.jp/)によりタイムスタンプ付与することを想定しています。 +| 識別子 | 場所 | 値の例 | +|:-------------------|:----------------------------------------------|:------------| +| `short_name` | osf.io AddonのAppConfig | `nextcloud` | +| `_provider` | osf.io AddonのFileNodeモデル | `nextcloud` | +| `addon_imp_name` | GravyValet Addon ImpのForeignAddonImpConfig | `NEXTCLOUD` | +| `wb_key` | GravyValet のExternalStorageService | `nextcloud` | +| エントリポイント名 | WaterButler Providerの`pyproject.toml` | `nextcloud` | -### ユーザによるタイムスタンプの追加 +より正確には以下の関係を満たす必要があります。 -タイムスタンプは、ファイルの内容から計算したハッシュ値を署名する形で作成されます。ハッシュの取得は以下のいずれかの方法でおこないます。 +``` +short_name == _provider == addon_imp_name.lower() == wb_key == WaterButler エントリポイント名 +``` -- `osf.models.files.File` サブクラスの `get_hash_for_timestamp`により取得する -- WaterButlerを経由してファイルをダウンロードし、ハッシュ計算を行う +加えて、osf.io Addonにおいて以下の2つはいずれも`addons_`という形式にする必要があります(例: `addons_nextcloud`)。 -`osf.models.files.File` サブクラスはアドオンのモデルとしてハッシュ値の取得方法を定義するものです。本サンプルでは [models.py](osf.io/addon/models.py) にあります。 -ストレージにより容易にハッシュ相当の値を取得・管理する方法がある場合は、このモデルに`get_hash_for_timestamp(self)`メソッドを定義します。 -タイムスタンプ処理はこのメソッドを通じてハッシュ値を取得することができます。 -`get_hash_for_timestamp(self)`メソッドの実装は dropboxbusinessアドオンの [DropboxBusinessFileクラス](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/dropboxbusiness/models.py#L38) を参考にしてください。 ++ AppConfigの`label`クラス属性 ( 例: `NextcloudAddonAppConfig.label`) ++ FileNodeの`app_label`Metaオプション (例: `NextcloudFileNode.Meta.app_label`) -`osf.models.files.File`モデルに`get_hash_for_timestamp(self)`が定義されていない場合は、タイムスタンプ処理はWaterButlerを経由してファイルをダウンロードする方法を試行します。 +## osf.io Addonのモジュール構成 -### RDM以外でのファイル変更によるタイムスタンプの追加 +### AppConfig -RDM以外でのファイル変更時に実施する場合も、ファイルが変更されたことを示すためにタイムスタンプを付加したい場合があります。 -このような処理を実行するためには `website.util.timestamp` モジュールを使用します。 -例えばDropbox Businessアドオンでは以下のように実装しています。 +`apps.py`に、`BaseAddonAppConfig`を継承した`AppConfig`を定義し、`name`、`short_name`([前章](#識別名について)を参照)、`label`(= `addons_`)、`full_name`(画面表示用)を設定します。さらに、`ready()`メソッドで`rejoin_models()`を呼び出します。 -[dropboxbusiness/utils.py](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/dropboxbusiness/utils.py) -``` -... -from website.util import timestamp +NextCloudの場合は、`nextcloud_plugin`パッケージの [`src/nextcloud_plugin/addon/apps.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/apps.py) を参照してください。要点は以下のとおりです。 -... +```python +class NextcloudAddonAppConfig(BaseAddonAppConfig): + name = 'nextcloud_plugin.addon' + short_name = 'nextcloud' + label = 'addons_nextcloud' + full_name = 'Nextcloud' + categories = ['storage'] + # ... -file_info = { - 'file_id': file_node._id, - 'file_name': attrs.get('name'), - 'file_path': attrs.get('materialized'), - 'size': attrs.get('size'), - 'created': attrs.get('created_utc'), - 'modified': attrs.get('modified_utc'), - 'file_version': '', - 'provider': PROVIDER_NAME -} -# verified by admin -verify_result = timestamp.check_file_timestamp( - admin.id, node, file_info, verify_external_only=True) + def ready(self): + super().ready() + from .models import NextcloudFileNode, NextcloudFile, NextcloudFolder + from .typedmodel_workaround import rejoin_models + rejoin_models(NextcloudFileNode, NextcloudFile, NextcloudFolder) ``` -`check_file_timestamp(uid, node, data, verify_external_only=False)`関数は以下のパラメータを指定することができます。 +### Model の構成 -- `uid` ... タイムスタンプ操作を行うユーザを示すOSFUserのID -- `node` ... ファイルが所属するプロジェクトを示すNode -- `data` ... タイムスタンプにより署名検証するデータを示す辞書型データ -- `verify_external_only` ... タイムスタンプ検証情報の格納に`osf.models.RdmFileTimestamptokenVerifyResult`を使う場合はFalse, 使わない場合(`osf.models.files.File` サブクラスの`def set_timestamp(self, timestamp_data, timestamp_status, context)`に格納する場合)はTrueとする (デフォルトはFalse) +`models.py`に以下のModelを定義します。 -なお、`def set_timestamp(self, timestamp_data, timestamp_status, context)` の定義方法は dropboxbusinessアドオンの [DropboxBusinessFileクラス](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/dropboxbusiness/models.py#L38) を参考にしてください。 +- `UserSettings`: ユーザーに関する情報(認証情報等) +- `NodeSettings`: プロジェクトに関する情報 +- `アドオン名FileNode`: ファイル・フォルダオブジェクトの親定義 +- `アドオン名File`: ファイルオブジェクトの定義 +- `アドオン名Folder`: フォルダオブジェクトの定義 -また、タイムスタンプの付加処理はストレージのAPI呼び出しやタイムスタンプの付加処理などI/Oを伴うため、viewsモジュール内の関数など、リクエストハンドラとして振る舞う関数中で処理を行ってしまうと、他のハンドラが待たされる要因になります。 -このような状況に対応するため、RDMでは[Celery](https://docs.celeryproject.org/en/stable/)によるワーカーが用意されています。 -関数をCeleryタスクとして定義することで、時間がかかる処理はワーカーに委譲することができます。 -例えばDropbox Businessアドオンでは以下のように定義しています。 +ファイルノードの3クラス(`FileNode` / `File` / `Folder`)は、**明示的にプロキシモデル**として定義し、TypedModelに再登録する必要があります。具体的には以下のようにします。 -[dropboxbusiness/utils.py](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/dropboxbusiness/utils.py) -``` -... -from framework.celery_tasks import app as celery_app +- `アドオン名FileNode` の基底クラスに `TypedModelRejoinMixin` を含める。 +- 3 クラスとも `Meta.proxy = True` を指定する。 +- `アドオン名FileNode` に `_provider = ''`、`db_owner = 'osf'`、`Meta.app_label = 'addons_'` を設定する。 -... +この手順に従うことで、[AppConfig.ready()](#appconfig)で呼び出す`rejoin_models()`がTypedModelへの再登録を実施します。 -@celery_app.task(bind=True, base=AbortableTask) -def celery_check_updated_files(self, team_ids): - ... -``` +```python +class NextcloudFileNode(TypedModelRejoinMixin, BaseFileNode): + _provider = 'nextcloud' + db_owner = 'osf' + class Meta: + proxy = True + app_label = 'addons_nextcloud' -呼び出し側では以下のように関数を実行することで、ワーカーに処理を任せ、自身の処理を続行することができます。 +class NextcloudFolder(NextcloudFileNode, Folder): + class Meta: + proxy = True -[dropboxbusiness/views.py](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons/dropboxbusiness/views.py) -``` -utils.celery_check_updated_files.delay(team_ids) +class NextcloudFile(NextcloudFileNode, File): + version_identifier = 'version' + class Meta: + proxy = True ``` -# My MinIOアドオンの実装 +> `BaseFileNode`クラスはTypedModelで管理されていますが、そのままではMigrationがosf.io側に生成されてしまい、ストレージアドオンを独立したPythonパッケージとして配布できません。プロキシモデルに明示的な`app_label`を与えるとMigrationがストレージアドオン側に生成されますが、TypedModelはこのようなクラスの管理を止めてしまいます。`TypedModelRejoinMixin`と`ready()`内の`rejoin_models()`により、これらのクラスを`db_owner`(= `osf`)をキーとしてTypedModelのレジストリへ再登録し、この問題を回避しています。詳細は`nextcloud_plugin`の[`src/nextcloud_plugin/addon/typedmodel_workaround.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/typedmodel_workaround.py)を参照してください。 + +`UserSettings`と`NodeSettings`は、OAuth認証をしない場合であってもそれぞれ`BaseOAuthUserSettings`と`BaseOAuthNodeSettings`(`NodeSettings`はさらに`BaseStorageAddon`)を継承し、`oauth_provider`に認証プロバイダクラス(`アドオン名Provider`)を指定します。認証プロバイダクラスは`provider.py`に定義します(`models.py`に定義されている場合もあります)。OAuthを用いる場合は`osf.models.external.ExternalProvider`を、ユーザー名・パスワードなどを用いる場合は`osf.models.external.BasicAuthProviderMixin`を継承します。NextCloudはWebDAVのユーザー名・パスワード認証を用いるため、後者を使用します。 -ここでは、 `myminio` という識別名のアドオンの実装を例に説明します。アドオンの完全名は `My MinIO` とします。 +NextCloudアドオンでの具体例は`nextcloud_plugin`パッケージの以下のファイルを参照してください。 -アドオン名は様々な場所に埋め込まれています。アドオン名を変更したい場合は、以降で追加・変更するファイル名やコードの `myminio` 、 `My MinIO` 、 `MyMinIO` という文字列を変更してください。 +- [`src/nextcloud_plugin/addon/models.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/models.py) +- [`src/nextcloud_plugin/addon/provider.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/provider.py) -My MinIOアドオンは、AWS S3互換サービスと接続するアドオンなので、Amazon S3アドオンやS3 Compatible Storageアドオンを参考に実装することができます。Amazon S3アドオンやS3 Compatible Storageアドオンとの違いは以下の通りです。 +### シリアライザ -- My MinIOアドオンは簡単のため、アップロードの暗号化機能を実装しない。 -- Amazon S3アドオンやS3 Compatible Storageアドオンは、アカウントごとに接続するサービスやLocationなどを選択できるが、My MinIOはサービス側で指定した特定のMinIOサービスのみを扱う。 +`serializer.py`に`アドオン名Serializer`(`addons.base.serializer.StorageAddonSerializer`を継承)を定義します。これは、アドオンの`NodeSettings` / `UserSettings`を、設定画面やAPIが扱えるJSON形式に変換するものです。次節「Viewの構成」で用いる`addons.base.generic_views`は、このシリアライザを介して設定情報を入出力します。 -## OSF.ioへの実装 +主にシリアライズする情報は以下のとおりです。 -### addons.myminio モジュールの定義 +- **接続先フォルダ**(`serialized_folder`): プロジェクトに紐付けたフォルダの名前とパス。 +- **各種エンドポイントURL**(`addon_serialized_urls`): 認証情報の追加・一覧・インポート、認証解除、フォルダ一覧取得、設定保存などのURL。 +- **Node設定 / User設定**(`serialized_node_settings` / `serialized_user_settings`): 認証状態や接続先などの設定値。 -スケルトン アドオンと同様に、[アドオンの実装例(`osf.io/addon/`)](osf.io/addon/)を `addons/myminio` ディレクトリにコピーします。 +また、`credentials_are_valid()`により、保存された認証情報が有効か(実際にストレージへ接続できるか)を検証します。 -### RDMコードの変更 +NextCloudアドオンでの具体例は[`src/nextcloud_plugin/addon/serializer.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/serializer.py)を参照してください。 -スケルトン アドオンと同様に、RDMのコードをいくつか変更します。 -- [addons.json](https://github.com/RCOSDP/RDM-osf.io/blob/develop/addons.json) - - 変更例のサンプル: [addons.json](osf.io/config/addons.json) -- [framework/addons/data/addons.json](https://github.com/RCOSDP/RDM-osf.io/blob/develop/framework/addons/data/addons.json) - - 変更例のサンプル: [addons.json](osf.io/config/framework/addons/data/addons.json) -- [Dockerfile](https://github.com/RCOSDP/RDM-osf.io/blob/develop/Dockerfile) - - 変更例のサンプル: [Dockerfile](osf.io/config/Dockerfile) -- [api/base/settings/defaults.py](https://github.com/RCOSDP/RDM-osf.io/blob/develop/api/base/settings/defaults.py) - - 変更例のサンプル: [defaults.py](osf.io/config/api/base/settings/defaults.py) +### Viewの構成 -`api/base/settings/defaults.py` は、 `INSTALLED_APPS` の他に、 `ADDONS_FOLDER_CONFIGURABLE` 、 `ADDONS_OAUTH` にもアドオン名を追加します。変更例サンプルでは、Amazon S3アドオンに合わせて、スケルトン アドオンとは異なる方法で設定しています。 +`views.py`には、アドオンのアカウント設定やフォルダ取得などのエンドポイントを定義します。一部のViewは[シリアライザ](#シリアライザ)と`addons.base.generic_views`モジュールを利用することで簡潔に定義することができます。 +```python +import_auth = addons.base.generic_views.import_auth(SHORT_NAME, Serializer) ``` -INSTALLED_APPS += ('addons.myminio',) -ADDONS_FOLDER_CONFIGURABLE.append('myminio') -ADDONS_OAUTH.append('myminio') + +`generic_views`で提供していないView処理を追加したい場合や、View処理をカスタマイズしたい場合は、以下のように`views.py`に個別の関数を定義します。 + +```python +@must_have_addon(SHORT_NAME, 'user') +@must_have_addon(SHORT_NAME, 'node') +def nextcloud_folder_list(node_addon, user_addon, **kwargs): + """ Returns all the subsequent folders under the folder id passed. + Not easily generalizable due to `path` kwarg. + """ + path = request.args.get('path') + return node_addon.get_folders(path=path) ``` -他にもストレージアドオンでは、 [website/static/storageAddons.json](https://github.com/RCOSDP/RDM-osf.io/blob/develop/website/static/storageAddons.json) にも設定を追加する必要があります。 +NextCloud の場合の具体例は、`nextcloud_plugin` パッケージの以下のファイルを参照してください。 -変更例は [storageAddons.json](osf.io/config/website/static/storageAddons.json) を参照してください。 +- [`src/nextcloud_plugin/addon/routes.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/routes.py) +- [`src/nextcloud_plugin/addon/views.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon/views.py) -``` - "myminio": { - "fullName": "My MinIO", - "externalView": false - }, +### 設定モジュール + +ストレージアドオンごとの固有の設定は`settings/defaults.py` で定義とデフォルト値を設定し、osf.ioのDjango settings から`getattr` で上書きする形で読み込むようにします。これにより、サービス管理者がパッケージの中身を編集することなく、`api/base/settings/local.py`で設定を上書きできます。したがって、これらの設定項目はREADMEに明記する必要があります。 + +```python +# src/nextcloud_plugin/addon/settings/defaults.py +from django.conf import settings as _osf + +USE_SSL = getattr(_osf, 'NEXTCLOUD_USE_SSL', True) +MAX_UPLOAD_SIZE = getattr(_osf, 'NEXTCLOUD_MAX_UPLOAD_SIZE', 5 * 1024) ``` -> `externalView` を `true` に設定すると、FileViewerでファイルの外部ページリンクボタンが表示されるようになります。リンクを正しく動作させるには、WaterButlerのProviderを修正する必要があります。詳細は[GoogleDriveの実装](https://github.com/RCOSDP/RDM-waterbutler/blob/develop/waterbutler/providers/googledrive/metadata.py#L116)などを参照してください。 +> 接続先ホストや「選択可能なホストの一覧」などの**サービス単位の設定は、GravyValetの`ExternalStorageService`で管理します**。`ExternalStorageService`はAddon Impなどを元に作られる、DB上のデータです。つまり、そのようなデータはソースコードの形では保持しません。 -> FileViewerで、フォルダの操作はできるがファイルの操作ができない場合は、 `storageAddons.json` の設定が漏れている可能性があります。 +### Migration +FileNodeモデルが必要とするMigrationを以下の手順で作成し、パッケージに同梱します。 -### Migrationsファイルの作成 +1. osf.io AddonのMigration以外の部分を完成させる +2. 開発環境のosf.ioにosf.io Addonをインストールする + - インストール方法は[ストレージアドオンの利用方法](#ストレージアドオンの利用方法)を参照 +3. `makemigrations`を**アプリケーションラベル(`addons_`)を明示して**実行する + - `docker compose run --rm web python3 manage.py makemigrations addons_nextcloud` +4. 生成されたMigrationファイルからosf側への依存(`('osf', '0XXX_...')`)を手作業で取り除き、ストレージアドオンの一部として`addon/migrations/`に配置してパッケージに同梱する + - osf.ioの特定のMigration履歴にストレージアドオンパッケージを固定しないため -`makemigrations` コマンドを実行して、Migrationsファイルを作成します。 +## GravyValet Addon Impのモジュール構成 -``` -$ docker compose run --rm web python3 manage.py makemigrations -``` +GravyValet Addon Impは、`gravyvalet.addon_toolkit.interfaces.storage`の`StorageAddonHttpRequestorImp`(HTTPベース)または`StorageAddonClientRequestorImp[T]`(クライアントライブラリベース)を継承して実装します。GravyValet本体に組み込まれた既存のAddon Imp(`gravyvalet/addon_imps/storage/*.py`)は良い実装例です。例えばNextCloudはWebDAV を用いるため、`owncloud.py`のAddon Impが近い実装例となります。 -上記の出力中に以下のような出力が現れれば成功です。ストレージアドオンの場合、 `osf/migrations` と `addons/myminio/migrations` の2つのディレクトリ内にPythonファイルが作成されます。 +`apps.py`には`ForeignAddonImpConfig`を継承したAppConfigを定義し、以下を実装・設定します。 -> `osf/migrations` 配下に作成されるファイル名は、作成日時やRDMのバージョンによって異なります。 +- `imp`: AddonImp 実装クラスを返すプロパティ。 +- `addon_imp_name`: このAddon Impの一意な識別名(大文字、例: `NEXTCLOUD`)を返すプロパティ。`addon_service.common.known_imps.KnownAddonImps`に列挙された名前、および既存のストレージアドオンで使われている値と衝突しない値を返します。 +- `name`: Djangoアプリのインポートパス(例: `nextcloud_plugin.addon_imp`)。アイコンの静的ファイルは`static/{name}/icons/` に配置します。 +- `label`: Django アプリのラベル。**一意な値を明示的に設定します**(例: `nextcloud_addon_imp`)。Django はラベルを省略するとパスの末尾(`addon_imp`)を使うため、これを省略したストレージアドオンを複数使うとGravyValet が起動できなくなります。 +```python +class NextcloudForeignAddonImpConfig(ForeignAddonImpConfig): + name = "nextcloud_plugin.addon_imp" + label = "nextcloud_addon_imp" + + @property + def imp(self): + return NextcloudStorageImp + + @property + def addon_imp_name(self): + return "NEXTCLOUD" ``` -Migrations for 'osf': - osf/migrations/0214_auto_20201001_0007.py - - Create proxy model MyMinIOFileNode - - Alter field type on basefilenode - - Create proxy model MyMinIOFile - - Create proxy model MyMinIOFolder -Migrations for 'addons_myminio': - addons/myminio/migrations/0001_initial.py - - Create model NodeSettings - - Create model UserSettings - - Add field user_settings to nodesettings + +`addon_imp_name`と`name` の値はREADME にも明記する必要があります。`addon_imp_name`はサービス管理者がこのストレージアドオンを設定する際に利用する値であり、サービス管理者が知る必要のある値です。`name`は他の同様なストレージアドオンパッケージが同じ値を使うことを避ける必要があり、ストレージアドオンの開発者が知る必要のある値です。 + +`imp.py`で定義するAddon Imp本体ではストレージのブラウズ(`list_root_items` / `list_child_items` / `get_item_info`)や認証検証(`get_external_account_id`)などを実装します。また、`build_wb_config()` メソッドで、WaterButler Provider に渡す設定(`settings`)を生成します。NextCloud の場合、接続先フォルダ・ホスト・SSL 検証フラグを返します。 + +```python +async def build_wb_config(self) -> dict: + # ... + return { + "folder": f"/{folder_path}", + "host": root_host, + "verify_ssl": True, + } ``` -### 国際化メッセージファイルの作成 +具体的な実装例は`nextcloud_plugin` パッケージの以下のファイルを参照してください。 -国際化メッセージファイルの定義ファイルは以下のコマンドで生成することができます。 +- [`src/nextcloud_plugin/addon_imp/apps.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon_imp/apps.py) +- [`src/nextcloud_plugin/addon_imp/imp.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/addon_imp/imp.py) -> このセクションの実施時は各サービスを停止状態にしてください。 +## WaterButler Providerのモジュール構成 -``` -# メッセージ定義JSONなどをJavaScriptファイルへと変換する。各サービスが停止している状態で実施する。 -$ docker compose run --rm web invoke assets +WaterButler ProviderはWaterButlerが提供する`waterbutler.core.provider.BaseProvider`を継承して実装します。`metadata.py`にはフォルダ・ファイルのメタデータとリビジョンを表すクラスを、`provider.py`にはストレージへ接続し CRUD 操作を行うProviderクラスを定義します。 -# メッセージ定義テンプレートファイル website/translations/js_messages.pot を更新する -$ docker compose run --rm web pybabel extract -F ./website/settings/babel_js.cfg -o ./website/translations/js_messages.pot . +Provider が提供する主なメソッドは以下のとおりです。 -# メッセージ定義ファイル website/translations/ja/LC_MESSAGES/js_messages.po を更新する -$ docker compose run --rm web pybabel update -i ./website/translations/js_messages.pot -o ./website/translations/en/LC_MESSAGES/js_messages.po -l en -$ docker compose run --rm web pybabel update -i ./website/translations/js_messages.pot -o ./website/translations/ja/LC_MESSAGES/js_messages.po -l ja -``` +| 関数名 | 引数 | 戻り値 | 処理 | +|:------|:----|:------|:-----| +| `validate_v1_path` | path, **kwargs | WaterButlerPath | 文字列で与えられたパス情報(`path`)を検証し、属性付きの WaterButlerPath オブジェクトを返す。 | +| `validate_path` | path, **kwargs | WaterButlerPath | 同上(廃止予定の v0 仕様との互換性維持のため、2 つのメソッドに分かれている)。 | +| `download` | path, accept_url=False, version=None, range=None, **kwargs | Stream | 指定されたパス(`path`)のデータをダウンロードする。戻り値にはデータアクセス用の Stream を返す。 | +| `upload` | stream, path, conflict='replace', **kwargs | Metadata | 指定されたパス(`path`)に指定されたデータ(`stream`)をアップロードする。戻り値にはアップロードしたファイルを示す Metadata を返す。 | +| `delete` | path, confirm_delete=0, **kwargs | なし | 指定されたパス(`path`)のファイル・フォルダを削除する。 | +| `revisions` | path, **kwargs | List(Revision) | 指定されたパス(`path`)のリビジョン情報を取得する。 | +| `metadata` | path, revision=None, **kwargs | Metadata or List(Metadata) | 指定されたパス(`path`)のメタデータを取得する。`path` が file の場合 `Metadata`、directory の場合 `List(Metadata)` を返す。 | +| `create_folder` | path, folder_precheck=True, **kwargs | Metadata | 指定されたパスにフォルダを作成する。戻り値には作成したフォルダを示す Metadata を返す。 | +| `can_intra_copy` | dest_provider, path=None | Bool | 指定された送信先 Provider(`dest_provider`), パス(`path`)に対して intra_copy(内部コピー: ストレージサービス上でのコピー)が可能かどうかを判定する。これが False の場合、いったん一時ディレクトリに download して dest に upload する操作となる。 | +| `can_intra_move` | dest_provider, path=None | Bool | 指定された送信先 Provider(`dest_provider`), パス(`path`)に対して intra_move(内部移動: ストレージサービス中での移動)が可能かどうかを判定する。これが False の場合、いったん一時ディレクトリに download し dest に upload、コピー元ファイルを delete する操作となる。 | +| `intra_copy` | dest_provider, src_path, dest_path | Bool | 内部コピーを実施する。成功すれば True を返す。 | +| `intra_move` | dest_provider, src_path, dest_path | Bool | 内部移動を実施する。成功すれば True を返す。 | + +Providerクラスの`NAME`には[前述の識別名](#識別名について)を指定します(例: `nextcloud`)。 -変更例のサンプル [js_messages.po](osf.io/config/website/translations/ja/LC_MESSAGES/js_messages.po) を参考に日本語メッセージを追加してください。 +```python +class NextcloudProvider(provider.BaseProvider): + NAME = 'nextcloud' + def __init__(self, auth, credentials, settings, **kwargs): + super().__init__(auth, credentials, settings, **kwargs) + self.folder = settings['folder'] + self.verify_ssl = settings['verify_ssl'] + self.url = credentials['host'] + self._auth = aiohttp.BasicAuth(credentials['username'], credentials['password']) ``` -#: website/static/js/logActionsList_extract.js:246 -msgid "${user} linked the My MinIO bucket ${bucket} to ${node}" -msgstr "${user}がMy MinIOバケット(${bucket})を${node}にリンクしました" -#: website/static/js/logActionsList_extract.js:247 -msgid "${user} unselected the My MinIO bucket ${bucket} in ${node}" -msgstr "${user}が${node}のMy MinIOバケット(${bucket})の選択を解除しました" +WaterButlerは`pyproject.toml`の`[tool.poetry.plugins."waterbutler.providers"]`に登録されたエントリポイントからProviderを発見します。**このエントリポイント名が、[前述の識別名](#識別名について)になります。** -... +```toml +[tool.poetry.plugins."waterbutler.providers"] +nextcloud = "nextcloud_plugin.provider.provider:NextcloudProvider" ``` -このメッセージを追加後、`assets`サービスの再起動時にメッセージ定義が反映されます。 +NextCloud での実装例は`nextcloud_plugin` パッケージの以下のファイルを参照してください。 + +- [`src/nextcloud_plugin/provider/provider.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/provider/provider.py) +- [`src/nextcloud_plugin/provider/metadata.py`](https://github.com/chiku-samugari/nextcloud_plugin/blob/main/src/nextcloud_plugin/provider/metadata.py) + +## 認証情報・設定情報の委譲 -### アドオンのテスト +WaterButler Providerが実際のストレージに接続するには、認証情報(`credentials`)と設定情報(`settings`)が必要です。本ドキュメントの想定する構成においてはこれらは**GravyValetが管理し、WaterButlerへ渡します**(従来の構成ではosf.io Addonの`NodeSettings.serialize_waterbutler_credentials()` / `serialize_waterbutler_settings()`が担っていました)。 -以下のコマンドで、OSF.ioに追加したMy MinIOアドオンのユニットテストを実行できます。 +![認証情報・設定情報の委譲フロー(osf.io → GravyValet → WaterButler)](images/delegation.png) +- 設定情報(`settings`)はGravyValet Addon Impの`build_wb_config()`が生成 + - NextCloud の場合は接続先フォルダ・ホスト・SSL 検証フラグ +- 認証情報(`credentials`)はGravyValet が管理する認証アカウント(ユーザーが接続時に入力した値)から渡される + - NextCloud の場合はホスト・ユーザー名・パスワード + +WaterButler Provider側では、コンストラクタ(`__init__`)の引数`credentials`と`settings`に、それぞれDictionary型で受け取ります。NextCloudの場合は以下のように受け取ります。 + +```python +def __init__(self, auth, credentials, settings, **kwargs): + super().__init__(auth, credentials, settings, **kwargs) + self.folder = settings['folder'] # build_wb_config() が生成 + self.verify_ssl = settings['verify_ssl'] # build_wb_config() が生成 + self.url = credentials['host'] # GravyValet のアカウントから + self._auth = aiohttp.BasicAuth(credentials['username'], credentials['password']) ``` -$ docker compose run --rm web invoke test_module -m addons/myminio/tests/ + +## Recent Activity の記録 + +何らかのユーザー操作を契機としてアドオンに対して行われた操作は、Recent Activity という形で記録できます。Recent Activityは[NodeLog モデル](https://github.com/CenterForOpenScience/osf.io/blob/develop/osf/models/nodelog.py) により表現され、Node(プロジェクトに対応するモデル)の`add_log`メソッドで記録します。 + +```python +self.owner.add_log( + '{0}_{1}'.format(SHORT_NAME, action), + auth=auth, + params={ + 'project': self.owner.parent_id, + 'node': self.owner._id, + 'path': metadata['materialized'], + 'folder': self.folder_id, + 'urls': { + 'view': url, + 'download': url + '?action=download', + }, + }, +) ``` -## WaterButlerへの実装 +- `action` ... ログのアクション種別。`アドオン名_アクション名` の形式。 +- `params` ... ログのパラメータ。任意の dict を指定できる。 +- `auth` ... 操作を実施したユーザーの情報([framework.auth.Auth クラス](https://github.com/CenterForOpenScience/osf.io/blob/develop/framework/auth/core.py) のインスタンス)。 -### waterbutler.providers.myminio モジュールの定義 +> ログの表示(どのメッセージをどう描画するか)は`angular-osf`フロントエンドが担当します。`RDM-osf.io`の`アドオン名LogActionList.json` / `アドオン名AnonymousLogActionList.json`やpybabel によるJavaScriptメッセージの国際化は不要です。 -[Providerの実装例(`waterbutler/provider/`)](waterbutler/provider/) を `waterbutler/providers/myminio` にコピーします。また、[Providerのテストコード例(`waterbutler/tests`)](waterbutler/tests) を `tests/providers/myminio` にコピーします。 +# ストレージアドオンの利用方法 -### RDMコードの変更 +作成したストレージアドオンは独立したPythonパッケージなので、`pip`や`poetry`を利用して各サービスにインストールします。特に、osf.io Addonのための[Migrationを作成する必要がある](#migration)ので、ここでは開発環境へのインストール方法を、NextCloud用のストレージアドオンパッケージである[`nextcloud_plugin`](https://github.com/chiku-samugari/nextcloud_plugin)を例にとって説明します。[開発環境の準備](#開発環境の準備)のガイドに従って開発環境にてosf.io, GravyValet, WaterButler, angular-osfを起動しているものとします。特に、osf.io, GravyValet, WaterButlerがそれぞれのディレクトリ、すなわち独立したComposeプロジェクトとして起動されている前提であることに注意してください。 -[setup.py](https://github.com/RCOSDP/RDM-waterbutler/blob/develop/setup.py) に、アドオンのエントリポイント定義を追加します。`setup()` 関数の引数 `entry_points` に指定するDictionaryの `waterbutler.providers` キーに指定する配列に、以下を追加します。 +## osf.io -``` -'myminio = waterbutler.providers.myminio:MyMinIOProvider', -``` +本節での作業は`osf.io`ディレクトリにて実施します。 -変更例はサンプル [setup.py](waterbutler/config/setup.py) を参照してください。 +1. `docker-compose.override.yml`に、`nextcloud_plugin`のコードベースをボリュームとして追加 + ``` + services: + web: + volumes: + - /path/to/nextcloud_plugin/:/nextcloud_plugin:cached + api: + volumes: + - /path/to/nextcloud_plugin/:/nextcloud_plugin:cached + worker: + volumes: + - /path/to/nextcloud_plugin/:/nextcloud_plugin:cached + ``` -### docker-compose.override.yml の追加 +2. ストレージアドオンパッケージを`api`/`web`/`worker`コンテナにインストール + - `docker compose exec web pip install -e /nextcloud_plugin` + - `docker compose exec api pip install -e /nextcloud_plugin` + - `docker compose exec worker pip install -e /nextcloud_plugin` -変更したコードを使うよう、RDMの `docker-compose.override.yml` で WaterButler のサービスの `volumes` を指定します。 +3. `api/base/settings/local.py`の`INSTALLED_APPS`に、osf.io Addonパッケージを追加 -例えば、以下のようなディレクトリ階層の場合は、 + ```python + INSTALLED_APPS += ('nextcloud_plugin.addon',) + ``` -``` -. -├── RDM-osf.io -│   ├── docker-compose.override.yml -│   └── docker-compose.yml -└── RDM-waterbutler -``` +ここで使う値はAppConfigの`name`の値です。 -`RDM-osf.io/docker-compose.override.yml` の内容を以下のようにします。 +4. `api/base/settings/local.py`の`ADDONS_FOLDER_CONFIGURABLE`に、`short_name`の値を追加 -``` -version: "3.4" + ```python + ADDONS_FOLDER_CONFIGURABLE += ['nextcloud'] + ``` -services: - wb: - volumes: - - ../RDM-waterbutler:/code - wb_worker: - volumes: - - ../RDM-waterbutler:/code - wb_requirements: - volumes: - - ../RDM-waterbutler:/code -``` +5. `addons.json`の`addons`リストに`short_name`の値を追加する + ```json + "addons": [ ..., "nextcloud" ], + ``` -### アドオンのテスト +必要に応じて `addons_archivable` 等にも追加します。 -以下のコマンドで、WaterButlerに追加したMy MinIOアドオン Providerのユニットテストを実行できます。 +6. サービスを再起動する + - `docker compose restart` -``` -$ docker compose run --rm wb invoke test --provider myminio -``` +> GravyValetを利用する場合、`storageAddons.json`への追記は不要です。 +## GravyValet -# My MinIOアドオンの動作確認 +本節の作業は`gravyvalet`ディレクトリにて実施します。 -My MinIOアドオンの動作確認をしてみましょう。 +1. `docker-compose.override.yml`に、`nextcloud_plugin`のコードベースをボリュームとして追加 + ``` + services: + gravyvalet: + volumes: + - /path/to/nextcloud_plugin/:/nextcloud_plugin:cached + ``` +2. ストレージアドオンパッケージを`gravyvalet`コンテナにインストール + - `docker compose exec gravyvalet poetry add --editable /nextcloud_plugin` -## MinIOサービスの起動 +3. `app/settings.py`の`INSTALLED_APPS`に、Addon Impのパッケージ(`name`)を追加 -接続先のMinIOサービスを起動します。 + ```python + INSTALLED_APPS += ('nextcloud_plugin.addon_imp',) + ``` -``` -docker run -p 9001:9000 \ - -e "MINIO_ACCESS_KEY=minioadmin" \ - -e "MINIO_SECRET_KEY=minioadmin" \ - minio/minio server /data -``` +4. `app/settings.py`の`ADDON_IMPS`にエントリを追加する。**キーは`addon_imp_name`(`"NEXTCLOUD"`)、値は一意な5000以上の整数**(パッケージ名をキーにしないこと。前述の[命名規則](#識別名について)の注意を参照)。 -RDMと同じコンピュータで実行する場合、9000番ポートが競合してしまうので、MinIOサービスのホストへの割り当てポートは9001などに設定します。 -`MINIO_ACCESS_KEY` と `MINIO_SECRET_KEY` は認証に使うキーです。適宜書き換えてください。 + ```python + ADDON_IMPS = { + # ... + "NEXTCLOUD": 5001, + } + ``` -その他MinIOに関する詳しい説明は[MinIOのドキュメント](https://docs.min.io/)を参照してください。 +5. GravyValetのサービスを再起動します。 + - `docker compose restart` -## OSF.io Addonの設定 +6. GravyValetの管理画面`localhost:8004/admin`から、NextCloud用の`ExternalStorageService`を作成します。**`wb_key`には`short_name`の値(`"nextcloud"`)を指定します**(= WaterButlerのエントリポイント名)。NextCloudのように接続先ホストをユーザーごとに入力させる場合、Service Typeを`HOSTED`とし、`api_base_url`は空にします。また、NextCloudはユーザ名とパスワードでの認証を用いるのでCredentials Formatとしては`USERNAME_PASSWORD`を指定します。 -OSF.io Addonの設定をします。 `addons/myminio/settings/local-dist.py` を `addons/myminio/settings/local.py` にコピーし、 `HOST` プロパティに先ほど起動したMinIOサービスのホスト名を指定します。 +![NextCloud用のExternalStorageServiceの設定例](images/gravyvalet-add-external-storage-service.png) -``` -HOST = '192.168.168.167:9001' -``` +## WaterButler -RDMとMinIOサービスを同じ環境で実行している場合は、ループバックエイリアスを指定します。 +1. `docker-compose.override.yml`に、`nextcloud_plugin`のコードベースをボリュームとして追加 + ``` + services: + waterbutler: + volumes: + - /path/to/nextcloud_plugin/:/nextcloud_plugin:cached + ``` -## DBマイグレーション +2. ストレージアドオンパッケージを`waterbutler`コンテナにインストール + - `docker compose exec waterbutler poetry add --editable /nextcloud_plugin` -マイグレーションを実行し、Migrations定義をPostgreSQLサービスに反映します。 +3. WaterButlerのサービスを再起動 + - `docker compose restart` -``` -$ docker compose run --rm web python3 manage.py migrate -``` +> **明示的な再起動が必須です。** editable installでは、パッケージへのパスを記した`.pth`ファイルがインタプリタ起動時にのみ読み込まれます。そのため起動済みのプロセスはインストール後も新しいProviderを認識せず(`ProviderNotFound`となる)、必ずプロセスを再起動する必要があります。 -## サービスの再起動 +Providerは`waterbutler.providers`エントリポイントから自動的に発見されます。 -WaterButlerに追加したProviderを有効にするために、 `wb_requirements` を起動します。 +# NextCloud アドオンの動作確認 -``` -$ docker compose up wb_requirements -``` +NextCloud アドオンの動作確認をしてみましょう。 + +## NextCloud サービスの起動 -変更したファイルに関連するサービスを再起動します。 +接続先のNextCloudサービスを起動します。動作確認にはNextCloud公式のDockerイメージが手軽です。 ``` -$ docker compose restart assets web api wb +$ docker run -d --name nextcloud-test -p 8081:80 nextcloud:stable ``` -これでサービスへの反映は完了です。 +ブラウザで `http://localhost:8081` を開き、セットアップウィザードに従って管理者ユーザーを作成します。 + +> WaterButler / GravyValetの各コンテナからNextCloudへ到達できる必要があります。osf.ioと同じホストで実行する場合は、`localhost`ではなくコンテナから到達可能なアドレス(osf.ioのローカル開発で用いるループバックエイリアス`192.168.168.167`など)を使用します。また、NextCloud側の`trusted_domains`に、そのアドレス(例: `192.168.168.167:8081`)を追加する必要がある場合があります。 + +## ExternalStorageServiceの作成 + +前述の「[ストレージアドオンの利用方法](#gravyvalet)」に従い、`ExternalStorageService`を作成します(`wb_key`は`nextcloud`、Credentials Formatは`USERNAME_PASSWORD`, Service Typeは`HOSTED`、`api_base_url` は空)。 ## ストレージアドオンを試す -My MinIOアドオンを試すには、以下のような操作を実施します。 - -1. RDM Web UIにアクセスする `http://localhost:5000` -1. ユーザ設定ページを開く -1. Configure add-on accountsページを開く -1. My MinIOアドオンの認証情報を設定をする - ![Authorize Addon](images/authorize-addon.png) - 認証情報の設定ダイアログが表示されるので、MinIOサービスの `MINIO_ACCESS_KEY` と `MINIO_SECRET_KEY` を、それぞれ、`Access Key` と `Secret Key` フォームに入力して保存します。 - ![Filled Credential](images/filled-credential.png) - 成功すれば、切断ボタンと利用しているプロジェクトリストが表示されるエリアが表示されます。 - ![Successful Authorization Addon](images/successful-authorization-addon.png) -1. 適当なプロジェクトを作成する -1. Add-onsページを開く -1. My MinIOアドオンを有効化する - ![Enable Addon](images/enable-addon.png) -1. My MinIOアドオンの設定をする - ![Configure Addon](images/configure-addon.png) - `Import Account from Profile`ボタンから、アドオンの設定を行います。 - 接続に成功すると、ルート直下のフォルダリストが表示されるので、プロジェクトに紐付けるフォルダを選択し、保存します。このページでフォルダを作成することもできます。 - ![Select Current Folder](images/select-current-folder.png) - -これで、作成したプロジェクトでMy MinIOアドオンが使えるようになりました。 -プロジェクトページのFiles ウィジェットやFilesページから、My MinIOサービスに対してフォルダの作成やファイルのアップロード、削除、ダウンロードなどができるようになるはずです。 - -![Enabled Addon](images/enabled-addon.png) - -以上でMy MinIOアドオンの動作確認は完了です! +angular-osf の画面から、以下のような操作を実施します。 + +1. アカウント設定画面で、NextCloud アドオンのアカウントを接続します。`Host URL` には NextCloud のホスト(例: `http://192.168.168.167:8081/`)、ユーザー名・パスワードには NextCloud のログイン情報を入力します。 +2. 適当なプロジェクトを作成し、NextCloud アドオンを有効化します。 +3. フォルダピッカーで、プロジェクトに紐付けるフォルダを選択します。 +4. プロジェクトのFilesから、NextCloudに対してフォルダ作成・アップロード・ダウンロード・削除・移動・コピーなどができることを確認します。 + +以上でNextCloudアドオンの動作確認は完了です。 diff --git a/StorageAddon/images/architecture.mmd b/StorageAddon/images/architecture.mmd new file mode 100644 index 0000000..9424593 --- /dev/null +++ b/StorageAddon/images/architecture.mmd @@ -0,0 +1,13 @@ +flowchart LR + subgraph PLUGIN["nextcloud_plugin(単一の Python パッケージ)"] + direction TB + A["addon
(osf.io アドオン)"] + I["addon_imp
(GravyValet Addon Imp)"] + P["provider
(WaterButler Provider)"] + end + FE["angular-osf
(フロントエンド)"] --> OSF[("osf.io")] + FE --> GV[("GravyValet")] + FE --> WB[("WaterButler")] + A -. "pip install" .-> OSF + I -. "poetry install" .-> GV + P -. "poetry install" .-> WB diff --git a/StorageAddon/images/architecture.png b/StorageAddon/images/architecture.png new file mode 100644 index 0000000..3d8f73c Binary files /dev/null and b/StorageAddon/images/architecture.png differ diff --git a/StorageAddon/images/crud.png b/StorageAddon/images/crud.png deleted file mode 100644 index ecf4783..0000000 Binary files a/StorageAddon/images/crud.png and /dev/null differ diff --git a/StorageAddon/images/crud.uml b/StorageAddon/images/crud.uml deleted file mode 100644 index f41ad66..0000000 --- a/StorageAddon/images/crud.uml +++ /dev/null @@ -1,23 +0,0 @@ -# Build: -# $ cat crud.uml | docker run --rm -i think/plantuml -tpng > crud.png -@startuml - -actor User as user -participant "OSF.io Addon" as osf -participant "WaterButler Provider" as wb -database "External Storage" as storage - -osf --> user : Cookie -user -> wb : CRUD Request (+ Cookie) -activate wb -'wb -> osf : Request -'activate osf -'return Credentials and Settings -osf --> wb : Credentials and Settings -wb -> storage : CRUD Request -activate storage -return (Files) -return (Files) - - -@enduml diff --git a/StorageAddon/images/delegation.mmd b/StorageAddon/images/delegation.mmd new file mode 100644 index 0000000..6c08565 --- /dev/null +++ b/StorageAddon/images/delegation.mmd @@ -0,0 +1,14 @@ +sequenceDiagram + participant FE as angular-osf + participant OSF as osf.io + participant GV as GravyValet + participant WB as WaterButler + participant NC as NextCloud + FE->>OSF: ファイル操作要求(provider = nextcloud) + OSF->>GV: 認証情報・設定を要求(wb_key = nextcloud) + Note over GV: Addon Imp の build_wb_config() が settings を生成し、
認証アカウントから credentials を取得 + GV-->>OSF: credentials + settings + OSF->>WB: make_provider(credentials, settings) + WB->>NC: WebDAV でアクセス(CRUD) + NC-->>WB: 応答 + WB-->>FE: 応答 diff --git a/StorageAddon/images/delegation.png b/StorageAddon/images/delegation.png new file mode 100644 index 0000000..39f8a80 Binary files /dev/null and b/StorageAddon/images/delegation.png differ diff --git a/StorageAddon/images/gravyvalet-add-external-storage-service.png b/StorageAddon/images/gravyvalet-add-external-storage-service.png new file mode 100644 index 0000000..80c3a87 Binary files /dev/null and b/StorageAddon/images/gravyvalet-add-external-storage-service.png differ diff --git a/StorageAddon/images/naming.mmd b/StorageAddon/images/naming.mmd new file mode 100644 index 0000000..482dbce --- /dev/null +++ b/StorageAddon/images/naming.mmd @@ -0,0 +1,8 @@ +flowchart TB + KEY(["識別名 canonical key = nextcloud"]) + KEY --> SN["osf.io アドオン
short_name = nextcloud"] + KEY --> PV["osf.io アドオン
_provider = nextcloud"] + KEY --> WK["GravyValet
wb_key = nextcloud"] + KEY --> AI["GravyValet
addon_imp_name = NEXTCLOUD
.lower() = nextcloud"] + KEY --> EP["WaterButler
エントリポイント名 = nextcloud"] + KEY --> LB["osf.io アドオン
label / app_label = addons_nextcloud"] diff --git a/StorageAddon/images/naming.png b/StorageAddon/images/naming.png new file mode 100644 index 0000000..cc7599a Binary files /dev/null and b/StorageAddon/images/naming.png differ diff --git a/StorageAddon/images/osf_class.png b/StorageAddon/images/osf_class.png deleted file mode 100644 index 34d7af8..0000000 Binary files a/StorageAddon/images/osf_class.png and /dev/null differ diff --git a/StorageAddon/images/osf_class.uml b/StorageAddon/images/osf_class.uml deleted file mode 100644 index 27a08c3..0000000 --- a/StorageAddon/images/osf_class.uml +++ /dev/null @@ -1,96 +0,0 @@ -# Build: -# $ cat osf_class.uml | docker run --rm -i think/plantuml -tpng > osf_class.png -@startuml - -package addons.base.apps <> { - class BaseAddonAppConfig -} - -package addons.base <> { - object generic_views -} - -package addons.base.serializer <> { - class StorageAddonSerializer -} - -package addons.base.models <> { - class BaseNodeSettings - class BaseUserSettings - class BaseOAuthNodeSettings - class BaseOAuthUserSettings - class BaseStorageAddon -} - -package osf.models.files <> { - class BaseFileNode - class Folder - class File -} - - -package addons.$(addon_short_name).apps <> { - class AddonAppConfig -} - -package addons.$(addon_short_name).routes <> { - object api_routes -} - -package addons.$(addon_short_name).provider <> { - class "$(AddonShortName)Provider" -} - -package addons.$(addon_short_name).serializer <> { - class "$(AddonShortName)Serializer" -} - -package addons.$(addon_short_name).models <> { - class NodeSettings - class UserSettings - class "$(AddonShortName)FileNode" - class "$(AddonShortName)Folder" - class "$(AddonShortName)File" -} - -package addons.$(addon_short_name).views <> { - object set_config - object get_config - object import_auth - object deauthorize_node - object add_user_account - object account_list - object create_folder - object folder_list -} - -BaseAddonAppConfig <|-- AddonAppConfig - -StorageAddonSerializer <|-- "$(AddonShortName)Serializer" - -BaseNodeSettings <|-- BaseOAuthNodeSettings -BaseUserSettings <|-- BaseOAuthUserSettings - -BaseOAuthNodeSettings <|-- NodeSettings -BaseStorageAddon <|-- NodeSettings -NodeSettings ..> "$(AddonShortName)Provider" -NodeSettings ..> "$(AddonShortName)Serializer" - -BaseOAuthUserSettings <|-- UserSettings -UserSettings ...> "$(AddonShortName)Provider" -UserSettings ...> "$(AddonShortName)Serializer" - -BaseFileNode <|-- "$(AddonShortName)FileNode" -Folder <|-- "$(AddonShortName)Folder" -File <|-- "$(AddonShortName)File" -"$(AddonShortName)FileNode" <|-- "$(AddonShortName)Folder" -"$(AddonShortName)FileNode" <|-- "$(AddonShortName)File" - -AddonAppConfig ..> api_routes -AddonAppConfig ..> NodeSettings -AddonAppConfig ..> UserSettings -api_routes ..> "addons.$(addon_short_name).views" -"addons.$(addon_short_name).views" ..> generic_views -"addons.$(addon_short_name).views" ..> "$(AddonShortName)Serializer" - -@enduml diff --git a/StorageAddon/images/proxy-routing.mmd b/StorageAddon/images/proxy-routing.mmd new file mode 100644 index 0000000..4863354 --- /dev/null +++ b/StorageAddon/images/proxy-routing.mmd @@ -0,0 +1,7 @@ +flowchart LR + B["ブラウザ
http://localhost"] --> C{{"Caddy
単一オリジン
リバースプロキシ"}} + C -->|"/v2, /_/"| API[("osf.io API
:8000")] + C -->|"/v1/resources/*"| WB[("WaterButler
:7777")] + C -->|"/v1, /static/*/icons"| GV[("GravyValet
:8004")] + C -->|"/login /logout /oauth /api/v1 /download"| CAS[("osf.io web/CAS
:5000")] + C -->|"上記以外すべて"| NG[("angular-osf
:4200")] diff --git a/StorageAddon/images/proxy-routing.png b/StorageAddon/images/proxy-routing.png new file mode 100644 index 0000000..d6704a5 Binary files /dev/null and b/StorageAddon/images/proxy-routing.png differ diff --git a/StorageAddon/images/wb_class.png b/StorageAddon/images/wb_class.png deleted file mode 100644 index 235092b..0000000 Binary files a/StorageAddon/images/wb_class.png and /dev/null differ diff --git a/StorageAddon/images/wb_class.uml b/StorageAddon/images/wb_class.uml deleted file mode 100644 index 3888be6..0000000 --- a/StorageAddon/images/wb_class.uml +++ /dev/null @@ -1,45 +0,0 @@ -# Build: -# $ cat wb_class.uml | docker run --rm -i think/plantuml -tpng > wb_class.png -@startuml - -package waterbutler.core.metadata <> { - class BaseMetadata - class BaseFileMetadata - class BaseFolderMetadata - class BaseFileRevisionMetadata -} - -package waterbutler.core.provider <> { - class BaseProvider -} - - -package waterbutler.providers.$(addon_short_name).metadata <> { - class "$(AddonShortName)Metadata" - class "$(AddonShortName)FileMetadataHeaders" - class "$(AddonShortName)FileMetadata" - class "$(AddonShortName)FolderKeyMetadata" - class "$(AddonShortName)FolderMetadata" - class "$(AddonShortName)Revision" -} - -package waterbutler.providers.$(addon_short_name).provider <> { - class "$(AddonShortName)Provider" -} - - -BaseMetadata <|-- "$(AddonShortName)Metadata" -"$(AddonShortName)Metadata" <|-- "$(AddonShortName)FileMetadataHeaders" -BaseFileMetadata <|-- "$(AddonShortName)FileMetadataHeaders" -"$(AddonShortName)Metadata" <|-- "$(AddonShortName)FileMetadata" -BaseFileMetadata <|-- "$(AddonShortName)FileMetadata" -"$(AddonShortName)Metadata" <|-- "$(AddonShortName)FolderKeyMetadata" -BaseFolderMetadata <|-- "$(AddonShortName)FolderKeyMetadata" -"$(AddonShortName)Metadata" <|-- "$(AddonShortName)FolderMetadata" -BaseFolderMetadata <|-- "$(AddonShortName)FolderMetadata" -BaseFileRevisionMetadata <|-- "$(AddonShortName)Revision" - -BaseProvider <|-- "$(AddonShortName)Provider" -"$(AddonShortName)Provider" ..> "waterbutler.providers.$(addon_short_name).metadata" - -@enduml diff --git a/StorageAddon/osf.io/addon/__init__.py b/StorageAddon/osf.io/addon/__init__.py deleted file mode 100644 index 0bdb5eb..0000000 --- a/StorageAddon/osf.io/addon/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -SHORT_NAME = 'myminio' -FULL_NAME = 'My MinIO' - -default_app_config = 'addons.{}.apps.MyMinIOAddonAppConfig'.format(SHORT_NAME) diff --git a/StorageAddon/osf.io/addon/apps.py b/StorageAddon/osf.io/addon/apps.py deleted file mode 100644 index 7b9dc03..0000000 --- a/StorageAddon/osf.io/addon/apps.py +++ /dev/null @@ -1,70 +0,0 @@ -import os - -from addons.base.apps import BaseAddonAppConfig, generic_root_folder -from addons.myminio import SHORT_NAME, FULL_NAME -from addons.myminio import settings - -myminio_root_folder = generic_root_folder('myminio') - -HERE = os.path.dirname(os.path.abspath(__file__)) -TEMPLATE_PATH = os.path.join( - HERE, - 'templates' -) - - -class MyMinIOAddonAppConfig(BaseAddonAppConfig): - - name = 'addons.{}'.format(SHORT_NAME) - label = 'addons_{}'.format(SHORT_NAME) - full_name = FULL_NAME - short_name = SHORT_NAME - - owners = ['user', 'node'] - configs = ['accounts', 'node'] - categories = ['storage'] - - has_hgrid_files = True - - max_file_size = settings.MAX_FILE_SIZE - - node_settings_template = os.path.join(TEMPLATE_PATH, 'node_settings.mako') - user_settings_template = os.path.join(TEMPLATE_PATH, 'user_settings.mako') - - BUCKET_LINKED = 'myminio_bucket_linked' - BUCKET_UNLINKED = 'myminio_bucket_unlinked' - FILE_ADDED = 'myminio_file_added' - FILE_REMOVED = 'myminio_file_removed' - FILE_UPDATED = 'myminio_file_updated' - FOLDER_CREATED = 'myminio_folder_created' - NODE_AUTHORIZED = 'myminio_node_authorized' - NODE_DEAUTHORIZED = 'myminio_node_deauthorized' - NODE_DEAUTHORIZED_NO_USER = 'myminio_node_deauthorized_no_user' - actions = ( - BUCKET_LINKED, - BUCKET_UNLINKED, - FILE_ADDED, - FILE_REMOVED, - FILE_UPDATED, - FOLDER_CREATED, - NODE_AUTHORIZED, - NODE_DEAUTHORIZED, - NODE_DEAUTHORIZED_NO_USER - ) - - @property - def get_hgrid_data(self): - return myminio_root_folder - - @property - def routes(self): - from . import routes - return [routes.api_routes] - - @property - def user_settings(self): - return self.get_model('UserSettings') - - @property - def node_settings(self): - return self.get_model('NodeSettings') diff --git a/StorageAddon/osf.io/addon/migrations/__init__.py b/StorageAddon/osf.io/addon/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/StorageAddon/osf.io/addon/models.py b/StorageAddon/osf.io/addon/models.py deleted file mode 100644 index 0a610c3..0000000 --- a/StorageAddon/osf.io/addon/models.py +++ /dev/null @@ -1,146 +0,0 @@ -from django.db import models - -import addons.myminio.settings as settings -from addons.base import exceptions -from addons.base.models import (BaseOAuthNodeSettings, BaseOAuthUserSettings, - BaseStorageAddon) -from addons.myminio import SHORT_NAME, FULL_NAME -from addons.myminio.provider import MyMinIOProvider -from addons.myminio.serializer import MyMinIOSerializer -from addons.myminio.utils import bucket_exists, get_bucket_names -from framework.auth.core import Auth -from osf.models.files import File, Folder, BaseFileNode - - -class MyMinIOFileNode(BaseFileNode): - _provider = SHORT_NAME - - -class MyMinIOFolder(MyMinIOFileNode, Folder): - pass - - -class MyMinIOFile(MyMinIOFileNode, File): - version_identifier = 'version' - - -class UserSettings(BaseOAuthUserSettings): - oauth_provider = MyMinIOProvider - serializer = MyMinIOSerializer - - -class NodeSettings(BaseOAuthNodeSettings, BaseStorageAddon): - oauth_provider = MyMinIOProvider - serializer = MyMinIOSerializer - - folder_id = models.TextField(blank=True, null=True) - folder_name = models.TextField(blank=True, null=True) - folder_location = models.TextField(blank=True, null=True) - user_settings = models.ForeignKey(UserSettings, null=True, blank=True, on_delete=models.CASCADE) - - @property - def folder_path(self): - return self.folder_name - - @property - def display_name(self): - return u'{0}: {1}'.format(self.config.full_name, self.folder_id) - - def set_folder(self, folder_id, auth): - host = settings.HOST - if not bucket_exists(host, - self.external_account.oauth_key, - self.external_account.oauth_secret, folder_id): - error_message = ('We are having trouble connecting to that bucket. ' - 'Try a different one.') - raise exceptions.InvalidFolderError(error_message) - - self.folder_id = str(folder_id) - self.folder_name = folder_id - self.save() - - self.nodelogger.log(action='bucket_linked', extra={'bucket': str(folder_id)}, save=True) - - def get_folders(self, **kwargs): - # This really gets only buckets, not subfolders, - # as that's all we want to be linkable on a node. - try: - buckets = get_bucket_names(self) - except Exception: - raise exceptions.InvalidAuthError() - - return [ - { - 'addon': SHORT_NAME, - 'kind': 'folder', - 'id': bucket, - 'name': bucket, - 'path': bucket, - 'urls': { - 'folders': '' - } - } - for bucket in buckets - ] - - @property - def complete(self): - return self.has_auth and self.folder_id is not None - - def authorize(self, user_settings, save=False): - self.user_settings = user_settings - self.nodelogger.log(action='node_authorized', save=save) - - def clear_settings(self): - self.folder_id = None - self.folder_name = None - self.folder_location = None - - def deauthorize(self, auth=None, log=True): - """Remove user authorization from this node and log the event.""" - self.clear_settings() - self.clear_auth() # Also performs a save - - if log: - self.nodelogger.log(action='node_deauthorized', save=True) - - def delete(self, save=True): - self.deauthorize(log=False) - super(NodeSettings, self).delete(save=save) - - def serialize_waterbutler_credentials(self): - if not self.has_auth: - raise exceptions.AddonError('Cannot serialize credentials for {} addon'.format(FULL_NAME)) - return { - 'host': settings.HOST, - 'access_key': self.external_account.oauth_key, - 'secret_key': self.external_account.oauth_secret, - } - - def serialize_waterbutler_settings(self): - if not self.folder_id: - raise exceptions.AddonError('Cannot serialize settings for {} addon'.format(FULL_NAME)) - return { - 'bucket': self.folder_id - } - - def create_waterbutler_log(self, auth, action, metadata): - url = self.owner.web_url_for('addon_view_or_download_file', path=metadata['path'], provider=SHORT_NAME) - - self.owner.add_log( - '{0}_{1}'.format(SHORT_NAME, action), - auth=auth, - params={ - 'project': self.owner.parent_id, - 'node': self.owner._id, - 'path': metadata['materialized'], - 'bucket': self.folder_id, - 'urls': { - 'view': url, - 'download': url + '?action=download' - } - }, - ) - - def after_delete(self, user): - self.deauthorize(Auth(user=user), log=True) diff --git a/StorageAddon/osf.io/addon/provider.py b/StorageAddon/osf.io/addon/provider.py deleted file mode 100644 index ae32265..0000000 --- a/StorageAddon/osf.io/addon/provider.py +++ /dev/null @@ -1,22 +0,0 @@ -from addons.myminio import SHORT_NAME, FULL_NAME -from addons.myminio.serializer import MyMinIOSerializer - - -class MyMinIOProvider(object): - """An alternative to `ExternalProvider` not tied to OAuth""" - - name = FULL_NAME - short_name = SHORT_NAME - serializer = MyMinIOSerializer - - def __init__(self, account=None): - super(MyMinIOProvider, self).__init__() - - # provide an unauthenticated session by default - self.account = account - - def __repr__(self): - return '<{name}: {status}>'.format( - name=self.__class__.__name__, - status=self.account.provider_id if self.account else 'anonymous' - ) diff --git a/StorageAddon/osf.io/addon/requirements.txt b/StorageAddon/osf.io/addon/requirements.txt deleted file mode 100644 index 8e6ed87..0000000 --- a/StorageAddon/osf.io/addon/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -boto==2.38.0 diff --git a/StorageAddon/osf.io/addon/routes.py b/StorageAddon/osf.io/addon/routes.py deleted file mode 100644 index ac287eb..0000000 --- a/StorageAddon/osf.io/addon/routes.py +++ /dev/null @@ -1,79 +0,0 @@ -from addons.myminio import SHORT_NAME -from addons.myminio import views -from framework.routing import Rule, json_renderer - -api_routes = { - 'rules': [ - Rule( - [ - '/settings/{}/accounts/'.format(SHORT_NAME), - ], - 'post', - views.myminio_add_user_account, - json_renderer, - ), - Rule( - [ - '/settings/{}/accounts/'.format(SHORT_NAME), - ], - 'get', - views.myminio_account_list, - json_renderer, - ), - Rule( - [ - '/project//{}/settings/'.format(SHORT_NAME), - '/project//node//{}/settings/'.format(SHORT_NAME), - ], - 'put', - views.myminio_set_config, - json_renderer, - ), - Rule( - [ - '/project//{}/settings/'.format(SHORT_NAME), - '/project//node//{}/settings/'.format(SHORT_NAME), - ], - 'get', - views.myminio_get_config, - json_renderer, - ), - Rule( - [ - '/project//{}/user-auth/'.format(SHORT_NAME), - '/project//node//{}/user-auth/'.format(SHORT_NAME), - ], - 'put', - views.myminio_import_auth, - json_renderer, - ), - Rule( - [ - '/project//{}/user-auth/'.format(SHORT_NAME), - '/project//node//{}/user-auth/'.format(SHORT_NAME), - ], - 'delete', - views.myminio_deauthorize_node, - json_renderer, - ), - Rule( - [ - '/project//{}/buckets/'.format(SHORT_NAME), - '/project//node//{}/buckets/'.format(SHORT_NAME), - ], - 'get', - views.myminio_folder_list, - json_renderer, - ), - Rule( - [ - '/project//{}/newbucket/'.format(SHORT_NAME), - '/project//node//{}/newbucket/'.format(SHORT_NAME), - ], - 'post', - views.myminio_create_bucket, - json_renderer - ), - ], - 'prefix': '/api/v1', -} diff --git a/StorageAddon/osf.io/addon/serializer.py b/StorageAddon/osf.io/addon/serializer.py deleted file mode 100644 index 8a50c4a..0000000 --- a/StorageAddon/osf.io/addon/serializer.py +++ /dev/null @@ -1,45 +0,0 @@ -from addons.base.serializer import StorageAddonSerializer -from addons.myminio import SHORT_NAME -from addons.myminio import settings -from addons.myminio import utils -from website.util import web_url_for - - -class MyMinIOSerializer(StorageAddonSerializer): - addon_short_name = SHORT_NAME - - REQUIRED_URLS = [] - - @property - def addon_serialized_urls(self): - node = self.node_settings.owner - user_settings = self.node_settings.user_settings or self.user_settings - - result = { - 'accounts': node.api_url_for('{}_account_list'.format(SHORT_NAME)), - 'createBucket': node.api_url_for('{}_create_bucket'.format(SHORT_NAME)), - 'importAuth': node.api_url_for('{}_import_auth'.format(SHORT_NAME)), - 'create': node.api_url_for('{}_add_user_account'.format(SHORT_NAME)), - 'deauthorize': node.api_url_for('{}_deauthorize_node'.format(SHORT_NAME)), - 'folders': node.api_url_for('{}_folder_list'.format(SHORT_NAME)), - 'config': node.api_url_for('{}_set_config'.format(SHORT_NAME)), - 'files': node.web_url_for('collect_file_trees'), - } - if user_settings: - result['owner'] = web_url_for('profile_view_id', - uid=user_settings.owner._id) - return result - - def serialized_folder(self, node_settings): - return { - 'path': node_settings.folder_id, - 'name': node_settings.folder_name - } - - def credentials_are_valid(self, user_settings, client=None): - if user_settings: - for account in user_settings.external_accounts.all(): - if utils.can_list(settings.HOST, - account.oauth_key, account.oauth_secret): - return True - return False diff --git a/StorageAddon/osf.io/addon/settings/__init__.py b/StorageAddon/osf.io/addon/settings/__init__.py deleted file mode 100644 index 4d3fcfa..0000000 --- a/StorageAddon/osf.io/addon/settings/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -import logging -from .defaults import * # noqa - -logger = logging.getLogger(__name__) -try: - from .local import * # noqa -except ImportError: - logger.warn('No local.py settings file found') diff --git a/StorageAddon/osf.io/addon/settings/defaults.py b/StorageAddon/osf.io/addon/settings/defaults.py deleted file mode 100644 index 9eb235d..0000000 --- a/StorageAddon/osf.io/addon/settings/defaults.py +++ /dev/null @@ -1 +0,0 @@ -MAX_FILE_SIZE = 128 # MB diff --git a/StorageAddon/osf.io/addon/settings/local-dist.py b/StorageAddon/osf.io/addon/settings/local-dist.py deleted file mode 100644 index dde3504..0000000 --- a/StorageAddon/osf.io/addon/settings/local-dist.py +++ /dev/null @@ -1,2 +0,0 @@ -# MyMinIO app credentials -HOST = 'changeme' diff --git a/StorageAddon/osf.io/addon/static/comicon.png b/StorageAddon/osf.io/addon/static/comicon.png deleted file mode 100644 index 7acb4dc..0000000 Binary files a/StorageAddon/osf.io/addon/static/comicon.png and /dev/null differ diff --git a/StorageAddon/osf.io/addon/static/myminioAnonymousLogActionList.json b/StorageAddon/osf.io/addon/static/myminioAnonymousLogActionList.json deleted file mode 100644 index af3169c..0000000 --- a/StorageAddon/osf.io/addon/static/myminioAnonymousLogActionList.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "myminio_bucket_linked" : "A user linked an My MinIO bucket to a project", - "myminio_bucket_unlinked" : "A user unselected an My MinIO bucket in a project", - "myminio_file_added" : "A user added a file to an My MinIO bucket in a project", - "myminio_file_removed" : "A user removed a file in an My MinIO bucket in a project", - "myminio_file_updated" : "A user updated a file in an My MinIO bucket in a project", - "myminio_folder_created" : "A user created a folder in an My MinIO in a project", - "myminio_node_authorized" : "A user authorized the My MinIO addon for a project", - "myminio_node_deauthorized" : "A user deauthorized the My MinIO addon for a project", - "myminio_node_deauthorized_no_user" : "My MinIO addon for a project deauthorized" -} diff --git a/StorageAddon/osf.io/addon/static/myminioLogActionList.json b/StorageAddon/osf.io/addon/static/myminioLogActionList.json deleted file mode 100644 index 09de23f..0000000 --- a/StorageAddon/osf.io/addon/static/myminioLogActionList.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "myminio_bucket_linked" : "${user} linked the My MinIO bucket ${bucket} to ${node}", - "myminio_bucket_unlinked" : "${user} unselected the My MinIO bucket ${bucket} in ${node}", - "myminio_file_added" : "${user} added file ${path} to My MinIO bucket ${bucket} in ${node}", - "myminio_file_removed" : "${user} removed ${path} in My MinIO bucket ${bucket} in ${node}", - "myminio_file_updated" : "${user} updated file ${path} in My MinIO bucket ${bucket} in ${node}", - "myminio_folder_created" : "${user} created folder ${path} in My MinIO bucket ${bucket} in ${node}", - "myminio_node_authorized" : "${user} authorized the My MinIO addon for ${node}", - "myminio_node_deauthorized" : "${user} deauthorized the My MinIO addon for ${node}", - "myminio_node_deauthorized_no_user" : "My MinIO addon for ${node} deauthorized" -} diff --git a/StorageAddon/osf.io/addon/static/myminioNodeConfig.js b/StorageAddon/osf.io/addon/static/myminioNodeConfig.js deleted file mode 100644 index 7c567cd..0000000 --- a/StorageAddon/osf.io/addon/static/myminioNodeConfig.js +++ /dev/null @@ -1,244 +0,0 @@ -'use strict'; - -var $ = require('jquery'); -var ko = require('knockout'); -var m = require('mithril'); -var bootbox = require('bootbox'); -var Raven = require('raven-js'); -var $osf = require('js/osfHelpers'); -var oop = require('js/oop'); - -var OauthAddonFolderPicker = require('js/oauthAddonNodeConfig')._OauthAddonNodeConfigViewModel; - -var MyMinIOFolderPickerViewModel = oop.extend(OauthAddonFolderPicker, { - constructor: function(addonName, url, selector, folderPicker, opts, tbOpts) { - var self = this; - self.super.constructor(addonName, url, selector, folderPicker, tbOpts); - // Non-OAuth fields - self.accessKey = ko.observable(''); - self.secretKey = ko.observable(''); - // Treebeard config - self.treebeardOptions = $.extend( - {}, - OauthAddonFolderPicker.prototype.treebeardOptions, - { // TreeBeard Options - columnTitles: function() { - return [{ - title: 'Buckets', - width: '75%', - sort: false - }, { - title: 'Select', - width: '25%', - sort: false - }]; - }, - resolveToggle: function(item) { - return ''; - }, - resolveIcon: function(item) { - return m('i.fa.fa-folder-o', ' '); - }, - }, - tbOpts - ); - }, - - connectAccount: function() { - var self = this; - if( !self.accessKey() && !self.secretKey() ){ - self.changeMessage('Please enter both an API access key and secret key.', 'text-danger'); - return; - } - - if (!self.accessKey() ){ - self.changeMessage('Please enter an API access key.', 'text-danger'); - return; - } - - if (!self.secretKey() ){ - self.changeMessage('Please enter an API secret key.', 'text-danger'); - return; - } - $osf.block(); - - return $osf.postJSON( - self.urls().create, { - secret_key: self.secretKey(), - access_key: self.accessKey() - } - ).done(function(response) { - $osf.unblock(); - self.clearModal(); - $('#myminioInputCredentials').modal('hide'); - self.changeMessage('Successfully added My MinIO credentials.', 'text-success', null, true); - self.updateFromData(response); - self.importAuth(); - }).fail(function(xhr, status, error) { - $osf.unblock(); - var message = ''; - var response = JSON.parse(xhr.responseText); - if (response && response.message) { - message = response.message; - } - self.changeMessage(message, 'text-danger'); - Raven.captureMessage('Could not add My MinIO credentials', { - extra: { - url: self.urls().importAuth, - textStatus: status, - error: error - } - }); - }); - }, - /** - * Tests if the given string is a valid My MinIO bucket name. Supports two modes: strict and lax. - * Strict is for bucket creation and follows the guidelines at: - * - * http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules - * - * However, the US East (N. Virginia) region currently permits much laxer naming rules. The S3 - * docs claim this will be changed at some point, but to support our user's already existing - * buckets, we provide the lax mode checking. - * - * Strict checking is the default. - * - * @param {String} bucketName user-provided name of bucket to validate - * @param {Boolean} laxChecking whether to use the more permissive validation - */ - isValidBucketName: function(bucketName, laxChecking) { - if (laxChecking === true) { - return /^[a-zA-Z0-9.\-_]{1,255}$/.test(bucketName); - } - var label = '[a-z0-9]+(?:[a-z0-9\-]*[a-z0-9])?'; - var strictBucketName = new RegExp('^' + label + '(?:\\.' + label + ')*$'); - var isIpAddress = /^[0-9]+(?:\.[0-9]+){3}$/; - return bucketName.length >= 3 && bucketName.length <= 63 && - strictBucketName.test(bucketName) && !isIpAddress.test(bucketName); - }, - - /** Reset all fields from My MinIO credentials input modal */ - clearModal: function() { - var self = this; - self.message(''); - self.messageClass('text-info'); - self.secretKey(null); - self.accessKey(null); - }, - - createBucket: function(self, bucketName) { - $osf.block(); - bucketName = bucketName.toLowerCase(); - return $osf.postJSON( - self.urls().createBucket, {bucket_name: bucketName} - ).done(function(response) { - $osf.unblock(); - self.loadedFolders(false); - self.activatePicker(); - var msg = 'Successfully created bucket "' + $osf.htmlEscape(bucketName) + '". You can now select it from the list.'; - var msgType = 'text-success'; - self.changeMessage(msg, msgType, null, true); - }).fail(function(xhr) { - var resp = JSON.parse(xhr.responseText); - var message = resp.message; - var title = resp.title || 'Problem creating bucket'; - $osf.unblock(); - if (!message) { - message = 'Looks like that name is taken. Try another name?'; - } - bootbox.confirm({ - title: $osf.htmlEscape(title), - message: $osf.htmlEscape(message), - callback: function(result) { - if (result) { - self.openCreateBucket(); - } - }, - buttons:{ - confirm:{ - label:'Try again' - } - } - }); - }); - }, - - openCreateBucket: function() { - var self = this; - - bootbox.dialog({ - title: 'Create a new bucket', - message: - '
' + - '
' + - '
' + - '
' + - ' ' + - '
' + - ' ' + - '
' + - '' + - '
'+ - '
' + - '
' + - '
' + - '
' + - '
', - buttons: { - cancel: { - label: 'Cancel', - className: 'btn-default' - }, - confirm: { - label: 'Create', - className: 'btn-success', - callback: function () { - var bucketName = $('#bucketName').val(); - - if (!bucketName) { - var errorMessage = $('#bucketModalErrorMessage'); - errorMessage.text('Bucket name cannot be empty'); - errorMessage[0].classList.add('text-danger'); - return false; - } else if (!self.isValidBucketName(bucketName, false)) { - bootbox.confirm({ - title: 'Invalid bucket name', - message: 'My MinIO buckets can contain lowercase letters, numbers, and hyphens separated by' + - ' periods. Please try another name.', - callback: function (result) { - if (result) { - self.openCreateBucket(); - } - }, - buttons: { - confirm: { - label: 'Try again' - } - } - }); - } else { - self.createBucket(self, bucketName); - } - } - } - } - }); - } -}); - -// Public API -function MyMinIONodeConfig(addonName, selector, url, folderPicker, opts, tbOpts) { - var self = this; - self.url = url; - self.folderPicker = folderPicker; - opts = opts || {}; - tbOpts = tbOpts || {}; - self.viewModel = new MyMinIOFolderPickerViewModel(addonName, url, selector, folderPicker, opts, tbOpts); - self.viewModel.updateFromData(); - $osf.applyBindings(self.viewModel, selector); -} - -module.exports = { - MyMinIONodeConfig: MyMinIONodeConfig, - _MyNinIONodeConfigViewModel: MyMinIOFolderPickerViewModel -}; diff --git a/StorageAddon/osf.io/addon/static/myminioUserConfig.js b/StorageAddon/osf.io/addon/static/myminioUserConfig.js deleted file mode 100644 index 980d65f..0000000 --- a/StorageAddon/osf.io/addon/static/myminioUserConfig.js +++ /dev/null @@ -1,172 +0,0 @@ -/** -* Module that controls the My MinIO user settings. Includes Knockout view-model -* for syncing data. -*/ - -var ko = require('knockout'); -var $ = require('jquery'); -var Raven = require('raven-js'); -var bootbox = require('bootbox'); -require('js/osfToggleHeight'); - -var language = require('js/osfLanguage').Addons.myminio; -var osfHelpers = require('js/osfHelpers'); -var addonSettings = require('js/addonSettings'); -var ChangeMessageMixin = require('js/changeMessage'); - -var ExternalAccount = addonSettings.ExternalAccount; - -var $modal = $('#myminioInputCredentials'); - - -function ViewModel(url) { - var self = this; - - self.properName = 'My MinIO'; - self.accessKey = ko.observable(); - self.secretKey = ko.observable(); - self.account_url = '/api/v1/settings/myminio/accounts/'; - self.accounts = ko.observableArray(); - - ChangeMessageMixin.call(self); - - /** Reset all fields from My MinIO credentials input modal */ - self.clearModal = function() { - self.message(''); - self.messageClass('text-info'); - self.accessKey(null); - self.secretKey(null); - }; - /** Send POST request to authorize My MinIO */ - self.connectAccount = function() { - // Selection should not be empty - if( !self.accessKey() && !self.secretKey() ){ - self.changeMessage('Please enter both an API access key and secret key.', 'text-danger'); - return; - } - - if (!self.accessKey() ){ - self.changeMessage('Please enter an API access key.', 'text-danger'); - return; - } - - if (!self.secretKey() ){ - self.changeMessage('Please enter an API secret key.', 'text-danger'); - return; - } - return osfHelpers.postJSON( - self.account_url, - ko.toJS({ - access_key: self.accessKey, - secret_key: self.secretKey, - }) - ).done(function() { - self.clearModal(); - $modal.modal('hide'); - self.updateAccounts(); - - }).fail(function(xhr, textStatus, error) { - var errorMessage = (xhr.status === 400 && xhr.responseJSON.message !== undefined) ? xhr.responseJSON.message : language.authError; - self.changeMessage(errorMessage, 'text-danger'); - Raven.captureMessage('Could not authenticate with My MinIO', { - extra: { - url: self.account_url, - textStatus: textStatus, - error: error - } - }); - }); - }; - - self.updateAccounts = function() { - return $.ajax({ - url: url, - type: 'GET', - dataType: 'json' - }).done(function (data) { - self.accounts($.map(data.accounts, function(account) { - var externalAccount = new ExternalAccount(account); - externalAccount.accessKey = account.oauth_key; - externalAccount.secretKey = account.oauth_secret; - return externalAccount; - })); - $('#myminio-header').osfToggleHeight({height: 160}); - }).fail(function(xhr, status, error) { - self.changeMessage(language.userSettingsError, 'text-danger'); - Raven.captureMessage('Error while updating addon account', { - extra: { - url: url, - status: status, - error: error - } - }); - }); - }; - - self.askDisconnect = function(account) { - var self = this; - bootbox.confirm({ - title: 'Disconnect My MinIO Account?', - message: '

' + - 'Are you sure you want to disconnect the My MinIO account ' + - osfHelpers.htmlEscape(account.name) + '? This will revoke access to My MinIO for all projects associated with this account.' + - '

', - callback: function (confirm) { - if (confirm) { - self.disconnectAccount(account); - } - }, - buttons:{ - confirm:{ - label:'Disconnect', - className:'btn-danger' - } - } - }); - }; - - self.disconnectAccount = function(account) { - var self = this; - var url = '/api/v1/oauth/accounts/' + account.id + '/'; - var request = $.ajax({ - url: url, - type: 'DELETE' - }); - request.done(function(data) { - self.updateAccounts(); - }); - request.fail(function(xhr, status, error) { - Raven.captureMessage('Error while removing addon authorization for ' + account.id, { - extra: { - url: url, - status: status, - error: error - } - }); - }); - return request; - }; - - self.selectionChanged = function() { - self.changeMessage('',''); - }; - - self.updateAccounts(); -} - -$.extend(ViewModel.prototype, ChangeMessageMixin.prototype); - -function MyMinIOUserConfig(selector, url) { - // Initialization code - var self = this; - self.selector = selector; - self.url = url; - // On success, instantiate and bind the ViewModel - self.viewModel = new ViewModel(url); - osfHelpers.applyBindings(self.viewModel, self.selector); -} - -module.exports = { - MyMinIOViewModel: ViewModel, - MyMinIOUserConfig: MyMinIOUserConfig -}; diff --git a/StorageAddon/osf.io/addon/static/node-cfg.js b/StorageAddon/osf.io/addon/static/node-cfg.js deleted file mode 100644 index 218df4d..0000000 --- a/StorageAddon/osf.io/addon/static/node-cfg.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -var MyMinIONodeConfig = require('./myminioNodeConfig.js').MyMinIONodeConfig; -var url = window.contextVars.node.urls.api + 'myminio/settings/'; -new MyMinIONodeConfig('My MinIO', '#myminioScope', url, '#myminioGrid'); diff --git a/StorageAddon/osf.io/addon/static/user-cfg.js b/StorageAddon/osf.io/addon/static/user-cfg.js deleted file mode 100644 index a21fee0..0000000 --- a/StorageAddon/osf.io/addon/static/user-cfg.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -var MyMinIOUserConfig = require('./myminioUserConfig.js').MyMinIOUserConfig; -var url = '/api/v1/settings/myminio/accounts/'; -new MyMinIOUserConfig('#myminioAddonScope', url); diff --git a/StorageAddon/osf.io/addon/templates/credentials_modal.mako b/StorageAddon/osf.io/addon/templates/credentials_modal.mako deleted file mode 100644 index a2892a5..0000000 --- a/StorageAddon/osf.io/addon/templates/credentials_modal.mako +++ /dev/null @@ -1,47 +0,0 @@ - diff --git a/StorageAddon/osf.io/addon/templates/node_settings.mako b/StorageAddon/osf.io/addon/templates/node_settings.mako deleted file mode 100644 index d10f294..0000000 --- a/StorageAddon/osf.io/addon/templates/node_settings.mako +++ /dev/null @@ -1,93 +0,0 @@ -
- - - <%include file="credentials_modal.mako"/> - -

- - ${addon_full_name} - - - authorized by - % if not is_registration: - ${_("Disconnect Account")} - % endif - - - - - - ${_("Import Account from Profile")} - - - - - -

- ${_("Loading ...")} -

-
- - - - - ${_("Connect Account")} - - -
-

- -
-
-
-

- ${_("Current Bucket:")} - - - - - ${_("None")} - -

- -
- - -
- -
-

- Loading buckets...

-
-
-
- -
-
-
-
- ${_("Connect %(folderName)s?") % dict(folderName='') | n} -
-
-
- - -
-
-
-
-
- -
- -
- -
-

-
-
diff --git a/StorageAddon/osf.io/addon/templates/user_settings.mako b/StorageAddon/osf.io/addon/templates/user_settings.mako deleted file mode 100644 index 49a00ca..0000000 --- a/StorageAddon/osf.io/addon/templates/user_settings.mako +++ /dev/null @@ -1,46 +0,0 @@ - -
- - <%include file="credentials_modal.mako"/> - -

- - - - ${_("Connect or Reauthorize Account")} - -

- -
- - ${_("Disconnect Account")} - -
- - - - - - - - - - - - - - -
- - ${_("Private project")} - - - - -
-
- -
-
diff --git a/StorageAddon/osf.io/addon/tests/__init__.py b/StorageAddon/osf.io/addon/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/StorageAddon/osf.io/addon/tests/conftest.py b/StorageAddon/osf.io/addon/tests/conftest.py deleted file mode 100644 index da9f243..0000000 --- a/StorageAddon/osf.io/addon/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from osf_tests.conftest import * # noqa diff --git a/StorageAddon/osf.io/addon/tests/factories.py b/StorageAddon/osf.io/addon/tests/factories.py deleted file mode 100644 index 4a90ea0..0000000 --- a/StorageAddon/osf.io/addon/tests/factories.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -"""Factories for the My MinIO addon.""" -import factory -from factory.django import DjangoModelFactory - -from addons.myminio import SHORT_NAME -from addons.myminio.models import ( - UserSettings, - NodeSettings -) -from osf_tests.factories import UserFactory, ProjectFactory, ExternalAccountFactory - - -class MyMinIOAccountFactory(ExternalAccountFactory): - provider = SHORT_NAME - provider_id = factory.Sequence(lambda n: 'id-{0}'.format(n)) - oauth_key = factory.Sequence(lambda n: 'key-{0}'.format(n)) - oauth_secret = factory.Sequence(lambda n: 'secret-{0}'.format(n)) - display_name = 'My MinIO Fake User' - - -class MyMinIOUserSettingsFactory(DjangoModelFactory): - class Meta: - model = UserSettings - - owner = factory.SubFactory(UserFactory) - - -class MyMinIONodeSettingsFactory(DjangoModelFactory): - class Meta: - model = NodeSettings - - owner = factory.SubFactory(ProjectFactory) - user_settings = factory.SubFactory(MyMinIOUserSettingsFactory) diff --git a/StorageAddon/osf.io/addon/tests/test_model.py b/StorageAddon/osf.io/addon/tests/test_model.py deleted file mode 100644 index b32e4f2..0000000 --- a/StorageAddon/osf.io/addon/tests/test_model.py +++ /dev/null @@ -1,104 +0,0 @@ -# from nose.tools import * # noqa -import unittest - -import mock -import pytest -from nose.tools import (assert_false, assert_true, - assert_equal, assert_is_none) - -from addons.base.tests.models import ( - OAuthAddonNodeSettingsTestSuiteMixin, - OAuthAddonUserSettingTestSuiteMixin -) -from addons.myminio import SHORT_NAME, FULL_NAME -from addons.myminio import settings -from addons.myminio.models import NodeSettings -from addons.myminio.tests.factories import ( - MyMinIOUserSettingsFactory, - MyMinIONodeSettingsFactory, - MyMinIOAccountFactory -) -from framework.auth import Auth -from osf_tests.factories import ProjectFactory, DraftRegistrationFactory -from tests.base import get_default_metaschema - -pytestmark = pytest.mark.django_db - - -class TestUserSettings(OAuthAddonUserSettingTestSuiteMixin, unittest.TestCase): - - short_name = SHORT_NAME - full_name = FULL_NAME - ExternalAccountFactory = MyMinIOAccountFactory - - -class TestNodeSettings(OAuthAddonNodeSettingsTestSuiteMixin, unittest.TestCase): - - short_name = SHORT_NAME - full_name = FULL_NAME - ExternalAccountFactory = MyMinIOAccountFactory - NodeSettingsFactory = MyMinIONodeSettingsFactory - NodeSettingsClass = NodeSettings - UserSettingsFactory = MyMinIOUserSettingsFactory - - def test_registration_settings(self): - registration = ProjectFactory() - clone, message = self.node_settings.after_register( - self.node, registration, self.user, - ) - assert_is_none(clone) - - def test_before_register_no_settings(self): - self.node_settings.user_settings = None - message = self.node_settings.before_register(self.node, self.user) - assert_false(message) - - def test_before_register_no_auth(self): - self.node_settings.external_account = None - message = self.node_settings.before_register(self.node, self.user) - assert_false(message) - - def test_before_register_settings_and_auth(self): - message = self.node_settings.before_register(self.node, self.user) - assert_true(message) - - @mock.patch('website.archiver.tasks.archive') - def test_does_not_get_copied_to_registrations(self, mock_archive): - registration = self.node.register_node( - schema=get_default_metaschema(), - auth=Auth(user=self.user), - draft_registration=DraftRegistrationFactory(branched_from=self.node), - ) - assert_false(registration.has_addon(SHORT_NAME)) - - ## Overrides ## - - def test_serialize_credentials(self): - self.user_settings.external_accounts[0].provider_id = 'user-11' - self.user_settings.external_accounts[0].oauth_key = 'key-11' - self.user_settings.external_accounts[0].oauth_secret = 'secret-15' - self.user_settings.save() - credentials = self.node_settings.serialize_waterbutler_credentials() - - expected = {'host': settings.HOST, - 'access_key': self.node_settings.external_account.oauth_key, - 'secret_key': self.node_settings.external_account.oauth_secret} - assert_equal(credentials, expected) - - @mock.patch('addons.myminio.models.bucket_exists') - def test_set_folder(self, mock_exists): - mock_exists.return_value = True - folder_id = '1234567890' - self.node_settings.set_folder(folder_id, auth=Auth(self.user)) - self.node_settings.save() - # Bucket was set - assert_equal(self.node_settings.folder_id, folder_id) - assert_equal(self.node_settings.folder_name, folder_id) - # Log was saved - last_log = self.node.logs.latest() - assert_equal(last_log.action, '{0}_bucket_linked'.format(self.short_name)) - - def test_serialize_settings(self): - settings = self.node_settings.serialize_waterbutler_settings() - expected = {'bucket': self.node_settings.folder_id} - assert_equal(settings, expected) diff --git a/StorageAddon/osf.io/addon/tests/test_serializer.py b/StorageAddon/osf.io/addon/tests/test_serializer.py deleted file mode 100644 index 4fca7b0..0000000 --- a/StorageAddon/osf.io/addon/tests/test_serializer.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -"""Serializer tests for the My MinIO addon.""" -import mock -import pytest - -from addons.base.tests.serializers import StorageAddonSerializerTestSuiteMixin -from addons.myminio.tests.factories import MyMinIOAccountFactory -from addons.myminio.serializer import MyMinIOSerializer - -from tests.base import OsfTestCase - -pytestmark = pytest.mark.django_db - -class TestMyMinIOSerializer(StorageAddonSerializerTestSuiteMixin, OsfTestCase): - addon_short_name = 'myminio' - Serializer = MyMinIOSerializer - ExternalAccountFactory = MyMinIOAccountFactory - client = None - - def set_provider_id(self, pid): - self.node_settings.folder_id = pid - - def setUp(self): - self.mock_can_list = mock.patch('addons.myminio.serializer.utils.can_list') - self.mock_can_list.return_value = True - self.mock_can_list.start() - super(TestMyMinIOSerializer, self).setUp() - - def tearDown(self): - self.mock_can_list.stop() - super(TestMyMinIOSerializer, self).tearDown() diff --git a/StorageAddon/osf.io/addon/tests/test_view.py b/StorageAddon/osf.io/addon/tests/test_view.py deleted file mode 100644 index 275acb2..0000000 --- a/StorageAddon/osf.io/addon/tests/test_view.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- -from rest_framework import status as http_status - -from boto.exception import S3ResponseError -import mock -from nose.tools import (assert_equal, assert_equals, - assert_true, assert_in, assert_false) -import pytest - -from framework.auth import Auth -from tests.base import OsfTestCase, get_default_metaschema -from osf_tests.factories import ProjectFactory, AuthUserFactory, DraftRegistrationFactory, InstitutionFactory - -from addons.base.tests.views import ( - OAuthAddonConfigViewsTestCaseMixin -) -from addons.myminio.tests.utils import MyMinIOAddonTestCase -from addons.myminio.utils import validate_bucket_name -import addons.myminio.settings as myminio_settings -from website.util import api_url_for -from admin.rdm_addons.utils import get_rdm_addon_option - -pytestmark = pytest.mark.django_db - -class TestMyMinIOViews(MyMinIOAddonTestCase, OAuthAddonConfigViewsTestCaseMixin, OsfTestCase): - def setUp(self): - self.mock_can_list = mock.patch('addons.myminio.views.utils.can_list') - self.mock_can_list.return_value = True - self.mock_can_list.start() - self.mock_uid = mock.patch('addons.myminio.views.utils.get_user_info') - self.mock_uid.return_value = {'id': '1234567890', 'display_name': 'myminio.user'} - self.mock_uid.start() - self.mock_exists = mock.patch('addons.myminio.views.utils.bucket_exists') - self.mock_exists.return_value = True - self.mock_exists.start() - super(TestMyMinIOViews, self).setUp() - - def tearDown(self): - self.mock_can_list.stop() - self.mock_uid.stop() - self.mock_exists.stop() - super(TestMyMinIOViews, self).tearDown() - - def test_myminio_settings_input_empty_keys(self): - url = self.project.api_url_for('myminio_add_user_account') - rv = self.app.post_json(url, { - 'access_key': '', - 'secret_key': '' - }, auth=self.user.auth, expect_errors=True) - assert_equals(rv.status_int, http_status.HTTP_400_BAD_REQUEST) - assert_in('All the fields above are required.', rv.body.decode()) - - def test_myminio_settings_input_empty_access_key(self): - url = self.project.api_url_for('myminio_add_user_account') - rv = self.app.post_json(url, { - 'access_key': '', - 'secret_key': 'Non-empty-secret-key' - }, auth=self.user.auth, expect_errors=True) - assert_equals(rv.status_int, http_status.HTTP_400_BAD_REQUEST) - assert_in('All the fields above are required.', rv.body.decode()) - - def test_myminio_settings_input_empty_secret_key(self): - url = self.project.api_url_for('myminio_add_user_account') - rv = self.app.post_json(url, { - 'access_key': 'Non-empty-access-key', - 'secret_key': '' - }, auth=self.user.auth, expect_errors=True) - assert_equals(rv.status_int, http_status.HTTP_400_BAD_REQUEST) - assert_in('All the fields above are required.', rv.body.decode()) - - def test_myminio_settings_rdm_addons_denied(self): - institution = InstitutionFactory() - self.user.affiliated_institutions.add(institution) - self.user.save() - rdm_addon_option = get_rdm_addon_option(institution.id, self.ADDON_SHORT_NAME) - rdm_addon_option.is_allowed = False - rdm_addon_option.save() - url = self.project.api_url_for('myminio_add_user_account') - rv = self.app.post_json(url,{ - 'access_key': 'aldkjf', - 'secret_key': 'las' - }, auth=self.user.auth, expect_errors=True) - assert_equal(rv.status_int, http_status.HTTP_403_FORBIDDEN) - assert_in('You are prohibited from using this add-on.', rv.body.decode()) - - def test_myminio_set_bucket_no_settings(self): - user = AuthUserFactory() - self.project.add_contributor(user, save=True) - url = self.project.api_url_for('myminio_set_config') - res = self.app.put_json( - url, {'myminio_bucket': 'hammertofall'}, auth=user.auth, - expect_errors=True - ) - assert_equal(res.status_code, http_status.HTTP_400_BAD_REQUEST) - - def test_myminio_set_bucket_no_auth(self): - - user = AuthUserFactory() - user.add_addon('myminio') - self.project.add_contributor(user, save=True) - url = self.project.api_url_for('myminio_set_config') - res = self.app.put_json( - url, {'myminio_bucket': 'hammertofall'}, auth=user.auth, - expect_errors=True - ) - assert_equal(res.status_code, http_status.HTTP_403_FORBIDDEN) - - def test_myminio_set_bucket_registered(self): - registration = self.project.register_node( - get_default_metaschema(), Auth(self.user), - DraftRegistrationFactory(branched_from=self.project), '' - ) - - url = registration.api_url_for('myminio_set_config') - res = self.app.put_json( - url, {'myminio_bucket': 'hammertofall'}, auth=self.user.auth, - expect_errors=True, - ) - - assert_equal(res.status_code, http_status.HTTP_400_BAD_REQUEST) - - @mock.patch('addons.myminio.views.utils.can_list', return_value=False) - def test_user_settings_cant_list(self, mock_can_list): - url = api_url_for('myminio_add_user_account') - rv = self.app.post_json(url, { - 'access_key': 'aldkjf', - 'secret_key': 'las' - }, auth=self.user.auth, expect_errors=True) - - assert_in('Unable to list buckets.', rv.body.decode()) - assert_equals(rv.status_int, http_status.HTTP_400_BAD_REQUEST) - - def test_myminio_remove_node_settings_owner(self): - url = self.node_settings.owner.api_url_for('myminio_deauthorize_node') - self.app.delete(url, auth=self.user.auth) - result = self.Serializer().serialize_settings(node_settings=self.node_settings, current_user=self.user) - assert_equal(result['nodeHasAuth'], False) - - def test_myminio_remove_node_settings_unauthorized(self): - url = self.node_settings.owner.api_url_for('myminio_deauthorize_node') - ret = self.app.delete(url, auth=None, expect_errors=True) - - assert_equal(ret.status_code, 401) - - def test_myminio_get_node_settings_owner(self): - self.node_settings.set_auth(self.external_account, self.user) - self.node_settings.folder_id = 'bucket' - self.node_settings.save() - url = self.node_settings.owner.api_url_for('myminio_get_config') - res = self.app.get(url, auth=self.user.auth) - - result = res.json['result'] - assert_equal(result['nodeHasAuth'], True) - assert_equal(result['userIsOwner'], True) - assert_equal(result['folder']['path'], self.node_settings.folder_id) - - def test_myminio_get_node_settings_unauthorized(self): - url = self.node_settings.owner.api_url_for('myminio_get_config') - unauthorized = AuthUserFactory() - ret = self.app.get(url, auth=unauthorized.auth, expect_errors=True) - - assert_equal(ret.status_code, 403) - - ## Overrides ## - - @mock.patch('addons.myminio.models.get_bucket_names') - def test_folder_list(self, mock_names): - mock_names.return_value = ['bucket1', 'bucket2'] - super(TestMyMinIOViews, self).test_folder_list() - - @mock.patch('addons.myminio.models.bucket_exists') - def test_set_config(self, mock_exists): - mock_exists.return_value = True - self.node_settings.set_auth(self.external_account, self.user) - url = self.project.api_url_for('{0}_set_config'.format(self.ADDON_SHORT_NAME)) - res = self.app.put_json(url, { - 'selected': self.folder - }, auth=self.user.auth) - assert_equal(res.status_code, http_status.HTTP_200_OK) - self.project.reload() - self.node_settings.reload() - assert_equal( - self.project.logs.latest().action, - '{0}_bucket_linked'.format(self.ADDON_SHORT_NAME) - ) - assert_equal(res.json['result']['folder']['name'], self.node_settings.folder_name) - - -class TestCreateBucket(MyMinIOAddonTestCase, OsfTestCase): - - def setUp(self): - - super(TestCreateBucket, self).setUp() - - self.user = AuthUserFactory() - self.consolidated_auth = Auth(user=self.user) - self.auth = self.user.auth - self.project = ProjectFactory(creator=self.user) - - self.project.add_addon('myminio', auth=self.consolidated_auth) - self.project.creator.add_addon('myminio') - - self.user_settings = self.user.get_addon('myminio') - self.user_settings.access_key = 'We-Will-Rock-You' - self.user_settings.secret_key = 'Idontknowanyqueensongs' - self.user_settings.save() - - self.node_settings = self.project.get_addon('myminio') - self.node_settings.bucket = 'Sheer-Heart-Attack' - self.node_settings.user_settings = self.project.creator.get_addon('myminio') - - self.node_settings.save() - - def test_bad_names(self): - assert_false(validate_bucket_name('')) - assert_false(validate_bucket_name('no')) - assert_false(validate_bucket_name('a' * 64)) - assert_false(validate_bucket_name(' leadingspace')) - assert_false(validate_bucket_name('trailingspace ')) - assert_false(validate_bucket_name('bogus naMe')) - assert_false(validate_bucket_name('.cantstartwithp')) - assert_false(validate_bucket_name('or.endwith.')) - assert_false(validate_bucket_name('..nodoubles')) - assert_false(validate_bucket_name('no_unders_in')) - assert_false(validate_bucket_name('-leadinghyphen')) - assert_false(validate_bucket_name('trailinghyphen-')) - assert_false(validate_bucket_name('Mixedcase')) - assert_false(validate_bucket_name('empty..label')) - assert_false(validate_bucket_name('label-.trailinghyphen')) - assert_false(validate_bucket_name('label.-leadinghyphen')) - assert_false(validate_bucket_name('8.8.8.8')) - assert_false(validate_bucket_name('600.9000.0.28')) - assert_false(validate_bucket_name('no_underscore')) - assert_false(validate_bucket_name('_nounderscoreinfront')) - assert_false(validate_bucket_name('no-underscore-in-back_')) - assert_false(validate_bucket_name('no-underscore-in_the_middle_either')) - - def test_names(self): - assert_true(validate_bucket_name('imagoodname')) - assert_true(validate_bucket_name('still.passing')) - assert_true(validate_bucket_name('can-have-dashes')) - assert_true(validate_bucket_name('kinda.name.spaced')) - assert_true(validate_bucket_name('a-o.valid')) - assert_true(validate_bucket_name('11.12.m')) - assert_true(validate_bucket_name('a--------a')) - assert_true(validate_bucket_name('a' * 63)) - - @mock.patch('addons.myminio.views.utils.create_bucket') - @mock.patch('addons.myminio.views.utils.get_bucket_names') - def test_create_bucket_pass(self, mock_names, mock_make): - mock_make.return_value = True - mock_names.return_value = [ - 'butintheend', - 'it', - 'doesntevenmatter' - ] - url = self.project.api_url_for('myminio_create_bucket') - ret = self.app.post_json( - url, - { - 'bucket_name': 'doesntevenmatter' - }, - auth=self.user.auth - ) - - assert_equal(ret.status_int, http_status.HTTP_200_OK) - assert_equal(ret.json, {}) - - @mock.patch('addons.myminio.views.utils.create_bucket') - def test_create_bucket_fail(self, mock_make): - error = S3ResponseError(418, 'because Im a test') - error.message = 'This should work' - mock_make.side_effect = error - - url = '/api/v1/project/{0}/myminio/newbucket/'.format(self.project._id) - ret = self.app.post_json(url, {'bucket_name': 'doesntevenmatter'}, auth=self.user.auth, expect_errors=True) - - assert_equals(ret.body.decode(), '{"message": "This should work", "title": "Problem connecting to My MinIO"}') diff --git a/StorageAddon/osf.io/addon/tests/utils.py b/StorageAddon/osf.io/addon/tests/utils.py deleted file mode 100644 index e9bbb16..0000000 --- a/StorageAddon/osf.io/addon/tests/utils.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -from nose.tools import (assert_equals, assert_true, assert_false) - -from addons.base.tests.base import OAuthAddonTestCaseMixin, AddonTestCase -from addons.myminio.tests.factories import MyMinIOAccountFactory -from addons.myminio.provider import MyMinIOProvider -from addons.myminio.serializer import MyMinIOSerializer -from addons.myminio import utils - -class MyMinIOAddonTestCase(OAuthAddonTestCaseMixin, AddonTestCase): - - ADDON_SHORT_NAME = 'myminio' - ExternalAccountFactory = MyMinIOAccountFactory - Provider = MyMinIOProvider - Serializer = MyMinIOSerializer - client = None - folder = { - 'path': 'bucket', - 'name': 'bucket', - 'id': 'bucket' - } - - def test_https(self): - connection = utils.connect_myminio(host='securehost', - access_key='a', - secret_key='s') - assert_true(connection.is_secure) - assert_equals(connection.host, 'securehost') - assert_equals(connection.port, 443) - - connection = utils.connect_myminio(host='securehost:443', - access_key='a', - secret_key='s') - assert_true(connection.is_secure) - assert_equals(connection.host, 'securehost') - assert_equals(connection.port, 443) - - def test_http(self): - connection = utils.connect_myminio(host='normalhost:80', - access_key='a', - secret_key='s') - assert_false(connection.is_secure) - assert_equals(connection.host, 'normalhost') - assert_equals(connection.port, 80) - - connection = utils.connect_myminio(host='normalhost:8080', - access_key='a', - secret_key='s') - assert_false(connection.is_secure) - assert_equals(connection.host, 'normalhost') - assert_equals(connection.port, 8080) diff --git a/StorageAddon/osf.io/addon/utils.py b/StorageAddon/osf.io/addon/utils.py deleted file mode 100644 index 6dad380..0000000 --- a/StorageAddon/osf.io/addon/utils.py +++ /dev/null @@ -1,131 +0,0 @@ -import re - -from boto import exception -from boto.s3.bucket import Bucket -from boto.s3.connection import S3Connection, OrdinaryCallingFormat, NoHostProvided -from rest_framework import status as http_status - -import addons.myminio.settings as settings -from framework.exceptions import HTTPError - - -class MyMinIOConnection(S3Connection): - def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, - is_secure=True, port=None, proxy=None, proxy_port=None, - proxy_user=None, proxy_pass=None, - host=NoHostProvided, debug=0, https_connection_factory=None, - calling_format=None, path='/', - provider='aws', bucket_class=Bucket, security_token=None, - suppress_consec_slashes=True, anon=False, - validate_certs=None, profile_name=None): - super(MyMinIOConnection, self).__init__(aws_access_key_id, - aws_secret_access_key, - is_secure, port, proxy, proxy_port, proxy_user, proxy_pass, - host=host, - debug=debug, https_connection_factory=https_connection_factory, - calling_format=calling_format, - path=path, provider=provider, bucket_class=bucket_class, - security_token=security_token, anon=anon, - validate_certs=validate_certs, profile_name=profile_name) - - def _required_auth_capability(self): - return ['s3'] - - -def connect_myminio(host=None, access_key=None, secret_key=None, node_settings=None): - """Helper to build an MyMinIOConnection object - """ - if host is None: - host = settings.HOST - if node_settings is not None and node_settings.external_account is not None: - access_key = node_settings.external_account.oauth_key - secret_key = node_settings.external_account.oauth_secret - port = 443 - m = re.match(r'^(.+)\:([0-9]+)$', host) - if m is not None: - host = m.group(1) - port = int(m.group(2)) - return MyMinIOConnection(access_key, secret_key, - calling_format=OrdinaryCallingFormat(), - host=host, - port=port, - is_secure=port == 443) - - -def get_bucket_names(node_settings): - try: - buckets = connect_myminio(node_settings=node_settings).get_all_buckets() - except exception.NoAuthHandlerFound: - raise HTTPError(http_status.HTTP_403_FORBIDDEN) - except exception.BotoServerError as e: - raise HTTPError(e.status) - - return [bucket.name for bucket in buckets] - - -def validate_bucket_name(name): - """Make sure the bucket name conforms to Amazon's expectations as described at: - http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules - The laxer rules for US East (N. Virginia) are not supported. - """ - label = r'[a-z0-9]+(?:[a-z0-9\-]*[a-z0-9])?' - validate_name = re.compile('^' + label + '(?:\\.' + label + ')*$') - is_ip_address = re.compile(r'^[0-9]+(?:\.[0-9]+){3}$') - return ( - len(name) >= 3 and len(name) <= 63 and bool(validate_name.match(name)) and not bool(is_ip_address.match(name)) - ) - - -def create_bucket(node_settings, bucket_name): - return connect_myminio(node_settings=node_settings).create_bucket(bucket_name) - - -def bucket_exists(host, access_key, secret_key, bucket_name): - """Tests for the existance of a bucket and if the user - can access it with the given keys - """ - if not bucket_name: - return False - - connection = connect_myminio(host, access_key, secret_key) - - if bucket_name != bucket_name.lower(): - # Must use ordinary calling format for mIxEdCaSe bucket names - # otherwise use the default as it handles bucket outside of the US - connection.calling_format = OrdinaryCallingFormat() - - try: - # Will raise an exception if bucket_name doesn't exist - connect_myminio(host, access_key, secret_key).head_bucket(bucket_name) - except exception.S3ResponseError as e: - if e.status not in (301, 302): - return False - return True - - -def can_list(host, access_key, secret_key): - """Return whether or not a user can list - all buckets accessable by this keys - """ - # Bail out early as boto does not handle getting - # Called with (None, None) - if not (host and access_key and secret_key): - return False - - try: - connect_myminio(host, access_key, secret_key).get_all_buckets() - except exception.S3ResponseError: - return False - return True - - -def get_user_info(host, access_key, secret_key): - """Returns an My MinIO User with .display_name and .id, or None - """ - if not (access_key and secret_key): - return None - - try: - return connect_myminio(host, access_key, secret_key).get_all_buckets().owner - except exception.S3ResponseError: - return None diff --git a/StorageAddon/osf.io/addon/views.py b/StorageAddon/osf.io/addon/views.py deleted file mode 100644 index 66afaee..0000000 --- a/StorageAddon/osf.io/addon/views.py +++ /dev/null @@ -1,152 +0,0 @@ -from boto import exception -from django.core.exceptions import ValidationError -from flask import request -from rest_framework import status as http_status - -import addons.myminio.settings as settings -from addons.base import generic_views -from addons.myminio import SHORT_NAME, FULL_NAME -from addons.myminio import utils -from addons.myminio.serializer import MyMinIOSerializer -from admin.rdm_addons.decorators import must_be_rdm_addons_allowed -from framework.auth.decorators import must_be_logged_in -from framework.exceptions import HTTPError -from osf.models import ExternalAccount -from website.project.decorators import ( - must_have_addon, must_have_permission, - must_be_addon_authorizer, -) - -myminio_account_list = generic_views.account_list( - SHORT_NAME, - MyMinIOSerializer -) - -myminio_import_auth = generic_views.import_auth( - SHORT_NAME, - MyMinIOSerializer -) - -myminio_deauthorize_node = generic_views.deauthorize_node( - SHORT_NAME -) - -myminio_get_config = generic_views.get_config( - SHORT_NAME, - MyMinIOSerializer -) - -def _set_folder(node_addon, folder, auth): - folder_id = folder['id'] - node_addon.set_folder(folder_id, auth=auth) - node_addon.save() - -myminio_set_config = generic_views.set_config( - SHORT_NAME, - FULL_NAME, - MyMinIOSerializer, - _set_folder -) - -@must_have_addon(SHORT_NAME, 'node') -@must_be_addon_authorizer(SHORT_NAME) -def myminio_folder_list(node_addon, **kwargs): - """ Returns all the subsequent folders under the folder id passed. - """ - return node_addon.get_folders() - -@must_be_logged_in -@must_be_rdm_addons_allowed(SHORT_NAME) -def myminio_add_user_account(auth, **kwargs): - """Verifies new external account credentials and adds to user's list""" - host = settings.HOST - try: - access_key = request.json['access_key'] - secret_key = request.json['secret_key'] - except KeyError: - raise HTTPError(http_status.HTTP_400_BAD_REQUEST) - - if not (access_key and secret_key): - return { - 'message': 'All the fields above are required.' - }, http_status.HTTP_400_BAD_REQUEST - - user_info = utils.get_user_info(host, access_key, secret_key) - if not user_info: - return { - 'message': ('Unable to access account.\n' - 'Check to make sure that the above credentials are valid, ' - 'and that they have permission to list buckets.') - }, http_status.HTTP_400_BAD_REQUEST - - if not utils.can_list(host, access_key, secret_key): - return { - 'message': ('Unable to list buckets.\n' - 'Listing buckets is required permission that can be changed via IAM') - }, http_status.HTTP_400_BAD_REQUEST - - try: - account = ExternalAccount( - provider=SHORT_NAME, - provider_name=FULL_NAME, - oauth_key=access_key, - oauth_secret=secret_key, - provider_id=user_info.id, - display_name=user_info.display_name, - ) - account.save() - except ValidationError: - # ... or get the old one - account = ExternalAccount.objects.get( - provider=SHORT_NAME, - provider_id=user_info.id - ) - if account.oauth_key != access_key or account.oauth_secret != secret_key: - account.oauth_key = access_key - account.oauth_secret = secret_key - account.save() - assert account is not None - - if not auth.user.external_accounts.filter(id=account.id).exists(): - auth.user.external_accounts.add(account) - - # Ensure My MinIO is enabled. - auth.user.get_or_add_addon('myminio', auth=auth) - auth.user.save() - - return {} - - -@must_be_addon_authorizer(SHORT_NAME) -@must_have_addon('myminio', 'node') -@must_have_permission('write') -def myminio_create_bucket(auth, node_addon, **kwargs): - bucket_name = request.json.get('bucket_name', '') - # bucket_location = request.json.get('bucket_location', '') - - if not utils.validate_bucket_name(bucket_name): - return { - 'message': 'That bucket name is not valid.', - 'title': 'Invalid bucket name', - }, http_status.HTTP_400_BAD_REQUEST - - try: - # utils.create_bucket(node_addon, bucket_name, bucket_location) - utils.create_bucket(node_addon, bucket_name) - except exception.S3ResponseError as e: - return { - 'message': e.message, - 'title': 'Problem connecting to My MinIO', - }, http_status.HTTP_400_BAD_REQUEST - except exception.S3CreateError as e: - return { - 'message': e.message, - 'title': "Problem creating bucket '{0}'".format(bucket_name), - }, http_status.HTTP_400_BAD_REQUEST - except exception.BotoClientError as e: # Base class catchall - return { - 'message': e.message, - 'title': 'Error connecting to My MinIO', - }, http_status.HTTP_400_BAD_REQUEST - - return {} diff --git a/StorageAddon/osf.io/config/Dockerfile b/StorageAddon/osf.io/config/Dockerfile deleted file mode 100644 index e914981..0000000 --- a/StorageAddon/osf.io/config/Dockerfile +++ /dev/null @@ -1,197 +0,0 @@ -FROM node:8-alpine3.9 - -ARG NODE_OPTIONS='--max-old-space-size=4096' - -# Source: https://github.com/docker-library/httpd/blob/7976cabe162268bd5ad2d233d61e340447bfc371/2.4/alpine/Dockerfile#L3 -RUN set -x \ - && addgroup -g 82 -S www-data \ - && adduser -h /var/www -u 82 -D -S -G www-data www-data - -RUN apk add --no-cache --virtual .run-deps \ - su-exec \ - bash \ - python3 \ - git \ - # lxml2 - libxml2 \ - libxslt \ - # psycopg2 - postgresql-libs \ - # cryptography - libffi \ - # gevent - libev \ - libevent \ - openblas-dev \ - && yarn global add bower - -WORKDIR /code - -COPY ./requirements.txt ./ -COPY ./requirements/ ./requirements/ -COPY ./addons/bitbucket/requirements.txt ./addons/bitbucket/ -COPY ./addons/box/requirements.txt ./addons/box/ -#COPY ./addons/citations/requirements.txt ./addons/citations/ -COPY ./addons/dataverse/requirements.txt ./addons/dataverse/ -COPY ./addons/dropbox/requirements.txt ./addons/dropbox/ -#COPY ./addons/dropboxbusiness/requirements.txt ./addons/dropboxbusiness/ -#COPY ./addons/figshare/requirements.txt ./addons/figshare/ -#COPY ./addons/forward/requirements.txt ./addons/forward/ -COPY ./addons/github/requirements.txt ./addons/github/ -COPY ./addons/gitlab/requirements.txt ./addons/gitlab/ -#COPY ./addons/googledrive/requirements.txt ./addons/googledrive/ -#COPY ./addons/iqbrims/requirements.txt ./addons/iqbrims/ -COPY ./addons/mendeley/requirements.txt ./addons/mendeley/ -COPY ./addons/onedrive/requirements.txt /code/addons/onedrive/ -#COPY ./addons/osfstorage/requirements.txt ./addons/osfstorage/ -COPY ./addons/owncloud/requirements.txt ./addons/owncloud/ -COPY ./addons/s3/requirements.txt ./addons/s3/ -COPY ./addons/twofactor/requirements.txt ./addons/twofactor/ -#COPY ./addons/wiki/requirements.txt ./addons/wiki/ -COPY ./addons/zotero/requirements.txt ./addons/zotero/ -COPY ./addons/swift/requirements.txt ./addons/swift/ -COPY ./addons/azureblobstorage/requirements.txt ./addons/azureblobstorage/ -COPY ./addons/weko/requirements.txt ./addons/weko/ -COPY ./addons/s3compat/requirements.txt ./addons/s3compat/ -COPY ./addons/nextcloud/requirements.txt ./addons/nextcloud/ -COPY ./addons/nextcloudinstitutions/requirements.txt ./addons/nextcloudinstitutions/ -COPY ./addons/myminio/requirements.txt ./addons/myminio/ - -RUN set -ex \ - && mkdir -p /var/www \ - && chown www-data:www-data /var/www \ - && apk add --no-cache --virtual .build-deps \ - build-base \ - linux-headers \ - python3-dev \ - # lxml2 - musl-dev \ - libxml2-dev \ - libxslt-dev \ - # psycopg2 - postgresql-dev \ - # cryptography - libffi-dev \ - libpng-dev \ - freetype-dev \ - jpeg-dev \ - && pip3 install Cython \ - && for reqs_file in \ - /code/requirements.txt \ - /code/requirements/release.txt \ - /code/addons/*/requirements.txt \ - ; do \ - pip3 install --no-cache-dir -c /code/requirements/constraints.txt -r "$reqs_file" \ - ; done \ - && (pip3 uninstall uritemplate.py --yes || true) \ - && pip3 install --no-cache-dir uritemplate.py==0.3.0 \ - # Fix: https://github.com/CenterForOpenScience/osf.io/pull/6783 - && python3 -m compileall /usr/lib/python3.6 || true \ - && apk del .build-deps - -# Settings -COPY ./tasks/ ./tasks/ -COPY ./website/settings/ ./website/settings/ -COPY ./api/base/settings/ ./api/base/settings/ -COPY ./website/__init__.py ./website/__init__.py -COPY ./addons.json ./addons.json -RUN mv ./website/settings/local-dist.py ./website/settings/local.py \ - && mv ./api/base/settings/local-dist.py ./api/base/settings/local.py \ - && sed 's/DEBUG_MODE = True/DEBUG_MODE = False/' -i ./website/settings/local.py - -# Bower Assets -COPY ./.bowerrc ./bower.json ./ -COPY ./admin/.bowerrc ./admin/bower.json ./admin/ -RUN \ - # OSF - bower install --production --allow-root \ - && bower cache clean --allow-root \ - # Admin - && cd ./admin \ - && bower install --production --allow-root \ - && bower cache clean --allow-root - -# Webpack Assets -# -## OSF -COPY ./package.json ./.yarnrc ./yarn.lock ./ -COPY ./webpack* ./ -COPY ./website/static/ ./website/static/ -COPY ./scripts/translations/ ./scripts/translations/ -COPY ./website/translations/ ./website/translations/ -COPY ./website/static/js/translations/ ./website/static/js/translations/ -## Admin -COPY ./admin/package.json ./admin/yarn.lock ./admin/ -COPY ./admin/webpack* ./admin/ -COPY ./admin/static/ ./admin/static/ -## Addons -COPY ./addons/bitbucket/static/ ./addons/bitbucket/static/ -COPY ./addons/box/static/ ./addons/box/static/ -COPY ./addons/citations/static/ ./addons/citations/static/ -COPY ./addons/dataverse/static/ ./addons/dataverse/static/ -COPY ./addons/dropbox/static/ ./addons/dropbox/static/ -COPY ./addons/dropboxbusiness/static/ ./addons/dropboxbusiness/static/ -COPY ./addons/figshare/static/ ./addons/figshare/static/ -COPY ./addons/forward/static/ ./addons/forward/static/ -COPY ./addons/github/static/ ./addons/github/static/ -COPY ./addons/gitlab/static/ ./addons/gitlab/static/ -COPY ./addons/googledrive/static/ ./addons/googledrive/static/ -COPY ./addons/mendeley/static/ ./addons/mendeley/static/ -COPY ./addons/onedrive/static/ /code/addons/onedrive/static/ -COPY ./addons/osfstorage/static/ ./addons/osfstorage/static/ -COPY ./addons/owncloud/static/ ./addons/owncloud/static/ -COPY ./addons/s3/static/ ./addons/s3/static/ -COPY ./addons/twofactor/static/ ./addons/twofactor/static/ -COPY ./addons/wiki/static/ ./addons/wiki/static/ -COPY ./addons/zotero/static/ ./addons/zotero/static/ -COPY ./addons/swift/static/ ./addons/swift/static/ -COPY ./addons/azureblobstorage/static/ ./addons/azureblobstorage/static/ -COPY ./addons/weko/static/ ./addons/weko/static/ -COPY ./addons/jupyterhub/static/ ./addons/jupyterhub/static/ -COPY ./addons/s3compat/static/ ./addons/s3compat/static/ -COPY ./addons/nextcloud/static/ ./addons/nextcloud/static/ -COPY ./addons/nextcloudinstitutions/static/ ./addons/nextcloudinstitutions/static/ -COPY ./addons/iqbrims/static/ ./addons/iqbrims/static/ -COPY ./addons/myminio/static/ ./addons/myminio/static/ -RUN \ - # OSF - yarn install --frozen-lockfile \ - && mkdir -p ./website/static/built/ \ - && invoke build_js_config_files \ - && yarn run webpack-prod \ - # Admin - && cd ./admin \ - && yarn install --frozen-lockfile \ - && yarn run webpack-prod \ - && cd ../ \ - # Cleanup - && yarn cache clean \ - && npm cache clean --force - -# Copy the rest of the code over -COPY ./ ./ - -ARG GIT_COMMIT= -ENV GIT_COMMIT ${GIT_COMMIT} - -RUN pybabel compile -d ./website/translations -RUN pybabel compile -D django -d ./admin/translations - -# TODO: Admin/API should fully specify their bower static deps, and not include ./website/static in their defaults.py. -# (this adds an additional 300+mb to the build image) -RUN for module in \ - api.base.settings \ - admin.base.settings \ - ; do \ - export DJANGO_SETTINGS_MODULE=$module \ - && python3 manage.py collectstatic --noinput --no-init-app \ - ; done \ - && for file in \ - ./website/templates/_log_templates.mako \ - ./website/static/built/nodeCategories.json \ - ; do \ - touch $file && chmod o+w $file \ - ; done \ - && rm ./website/settings/local.py ./api/base/settings/local.py - -CMD ["su-exec", "nobody", "invoke", "--list"] diff --git a/StorageAddon/osf.io/config/addons.json b/StorageAddon/osf.io/config/addons.json deleted file mode 100644 index 995974c..0000000 --- a/StorageAddon/osf.io/config/addons.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "addons": [ - "box", - "dataverse", - "dropbox", - "figshare", - "forward", - "github", - "gitlab", - "mendeley", - "zotero", - "osfstorage", - "owncloud", - "onedrive", - "s3", - "twofactor", - "wiki", - "googledrive", - "bitbucket", - "swift", - "azureblobstorage", - "weko", - "jupyterhub", - "s3compat", - "myminio", - "nextcloud", - "nextcloudinstitutions", - "iqbrims", - "dropboxbusiness" - ], - "addons_default": [ - "osfstorage", - "nextcloudinstitutions", - "dropboxbusiness" - ], - "addons_archivable": { - "osfstorage": "full", - "box": "partial", - "dataverse": "partial", - "dropbox": "partial", - "figshare": "partial", - "forward": "full", - "googledrive": "partial", - "onedrive": "partial", - "github": "partial", - "gitlab": "partial", - "owncloud": "partial", - "s3": "partial", - "wiki": "full", - "bitbucket": "partial", - "swift": "partial", - "azureblobstorage": "partial", - "weko": "partial", - "s3compat": "partial", - "myminio": "partial", - "nextcloud": "partial", - "nextcloudinstitutions": "partial", - "iqbrims": "partial", - "dropboxbusiness": "partial" - }, - "addons_commentable": [ - "osfstorage", - "box", - "dataverse", - "dropbox", - "figshare", - "github", - "gitlab", - "googledrive", - "owncloud", - "s3", - "bitbucket", - "onedrive", - "swift", - "swift", - "azureblobstorage", - "weko", - "s3compat", - "myminio", - "nextcloud", - "nextcloudinstitutions", - "iqbrims", - "dropboxbusiness" - ], - "addons_based_on_ids": [ - "osfstorage", - "box", - "onedrive" - ], - "addons_oauth_no_redirect": [ - "bitbucket" - ], - "addons_description": { - "box": "Box is a file storage add-on. Connect your Box account to a GakuNin RDM project to interact with files hosted on Box via the GakuNin RDM.", - "dataverse": "Dataverse is an open source software application to share, cite, and archive data. Connect your Dataverse account to share your Dataverse datasets via the GakuNin RDM.", - "dropbox": "Dropbox is a file storage add-on. Connect your Dropbox account to a GakuNin RDM project to interact with files hosted on Dropbox via the GakuNin RDM.", - "figshare": "Figshare is an online digital repository. Connect your figshare account to share your figshare files along with other materials in your GakuNin RDM project.", - "forward": "The External Link add-on allows you to provide a link to a website outside of the GakuNin RDM.", - "github": "GitHub is a web-based Git repository hosting service. Connect your GitHub repo to your GakuNin RDM project to share your code alongside other materials in your GakuNin RDM project.", - "gitlab": "GitLab is an open-source web-based Git repository hosting tool and service. Connect your GitLab repo to your GakuNin RDM project to share your code alongside other materials in your GakuNin RDM project.", - "mendeley": "Mendeley is a reference management tool. Connecting Mendeley folders to GakuNin RDM projects allows you and others to view, copy, and download citations that are relevant to your project from the Project Overview page.", - "zotero": "Zotero is a reference management tool. Connecting Zotero folders to GakuNin RDM projects allows you and others to view, copy, and download citations that are relevant to your project from the Project Overview page.", - "osfstorage": "NII Storage is the default storage provider for GakuNin RDM projects.", - "owncloud": "ownCloud is an open source, self-hosted file sync and share app platform. Connect your ownCloud account to a GakuNin RDM project to interact with files hosted on ownCloud via the GakuNin RDM.", - "s3": "Amazon S3 is a file storage add-on. Connect your S3 account to a GakuNin RDM project to interact with files hosted on S3 via the GakuNin RDM.", - "twofactor": "Two-factor authentication is a security add-on. By using two-factor authentication, you'll protect your GakuNin RDM account with both your password and your mobile phone.", - "wiki": "The wiki is a versatile communication tool. Wikis can be used to explain the main points of your project and can contain information like lab notes or contact information.", - "googledrive": "Google Drive is a file storage add-on. Connect your Google Drive account to a GakuNin RDM project to interact with files hosted on Google Drive via the GakuNin RDM.", - "bitbucket": "Bitbucket is a web-based Git repository hosting service. Connect your Bitbucket repo to your GakuNin RDM project to share your code alongside other materials in your GakuNin RDM project.", - "onedrive": "Microsoft OneDrive is a file storage add-on. Connect your Microsoft OneDrive account to a GakuNin RDM project to interact with files hosted on Microsoft OneDrive via the GakuNin RDM.", - "swift": "OpenStack Swift is a file storage add-on. Connect your OpenStack Swift account to a GakuNin RDM project to interact with files hosted on OpenStack Swift via the GakuNin RDM.", - "azureblobstorage": "Azure Blob Storage is a file storage add-on. Connect your Azure Storage account to a GakuNin RDM project to interact with files hosted on Azure Blob Storage (Block Blob) via the GakuNin RDM.", - "weko": "WEKO is an application server to share, archive data.", - "jupyterhub": "Jupyter is a web-based interactive computational environment. Files on a GakuNin RDM project can be imported to/exported from Jupyter", - "s3compat": "S3 Compatible Storage is a file storage add-on. Connect your S3 Compatible Storage account to a GakuNin RDM project to interact with files hosted on S3 Compatible Storage via the GakuNin RDM.", - "myminio": "My MinIO is a file storage add-on. Connect your My MinIO account to a GakuNin RDM project to interact with files hosted on My MinIO via the GakuNin RDM.", - "nextcloud": "Nextcloud is an open source, self-hosted file sync and share app platform. Connect your Nextcloud account to a GakuNin RDM project to interact with files hosted on Nextcloud via the GakuNin RDM.", - "iqbrims": "IQB-RIMS is a file storage add-on. Connect your Google Drive account to a GakuNin RDM project to interact with files hosted on Google Drive via the GakuNin RDM.", - "nextcloudinstitutions": "Nextcloud for Institutions is a file storage add-on for Institutions. Connect your Nextcloud account to a GakuNin RDM project to interact with files hosted on Nextcloud via the GakuNin RDM.", - "dropboxbusiness": "Dropbox Business for Institutions is a file storage add-on for Institutions. Connect your Dropbox Business administrator account to a GakuNin RDM project to interact with files hosted on Dropbox via the GakuNin RDM." - }, - "addons_url": { - "box": "http://www.box.com", - "dataverse": "https://dataverse.harvard.edu/", - "dropbox": "http://www.dropbox.com", - "figshare": "http://www.figshare.com", - "github": "http://www.github.com", - "gitlab": "https://www.gitlab.com", - "mendeley": "http://www.mendeley.com", - "owncloud": "https://owncloud.org/", - "zotero": "http://www.zotero.org", - "s3": "https://aws.amazon.com/s3/", - "googledrive": "https://drive.google.com", - "bitbucket": "https://bitbucket.org/", - "onedrive": "https://onedrive.live.com", - "swift": "https://wiki.openstack.org/wiki/Swift", - "azureblobstorage": "https://azure.microsoft.com/", - "weko": "https://weko.at.nii.ac.jp/", - "jupyterhub": "https://jupyterhub.readthedocs.io/", - "s3compat": "https://aws.amazon.com/s3/", - "myminio": "https://min.io/", - "nextcloud": "https://nextcloud.com/", - "iqbrims": "https://drive.google.com", - "dropboxbusiness": "http://www.dropbox.com" - } -} diff --git a/StorageAddon/osf.io/config/api/base/settings/defaults.py b/StorageAddon/osf.io/config/api/base/settings/defaults.py deleted file mode 100644 index d66c245..0000000 --- a/StorageAddon/osf.io/config/api/base/settings/defaults.py +++ /dev/null @@ -1,495 +0,0 @@ -""" -Django settings for api project. - -Generated by 'django-admin startproject' using Django 1.8. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ -""" - -import os -from future.moves.urllib.parse import urlparse -from website import settings as osf_settings - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -DATABASES = { - 'default': { - 'CONN_MAX_AGE': 0, - 'ENGINE': 'osf.db.backends.postgresql', # django.db.backends.postgresql - 'NAME': os.environ.get('OSF_DB_NAME', 'osf'), - 'USER': os.environ.get('OSF_DB_USER', 'postgres'), - 'PASSWORD': os.environ.get('OSF_DB_PASSWORD', ''), - 'HOST': os.environ.get('OSF_DB_HOST', '127.0.0.1'), - 'PORT': os.environ.get('OSF_DB_PORT', '5432'), - 'ATOMIC_REQUESTS': True, - 'TEST': { - 'SERIALIZE': False, - }, - }, -} - -DATABASE_ROUTERS = ['osf.db.router.PostgreSQLFailoverRouter', ] -PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', -] - -AUTH_USER_MODEL = 'osf.OSFUser' - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = osf_settings.SECRET_KEY - -AUTHENTICATION_BACKENDS = ( - 'api.base.authentication.backends.ODMBackend', - 'guardian.backends.ObjectPermissionBackend', -) - -# SECURITY WARNING: don't run with debug turned on in production! -DEV_MODE = osf_settings.DEV_MODE -DEBUG = osf_settings.DEBUG_MODE -DEBUG_PROPAGATE_EXCEPTIONS = True - -# session: -SESSION_COOKIE_NAME = 'api' -SESSION_COOKIE_SECURE = osf_settings.SECURE_MODE -SESSION_COOKIE_HTTPONLY = osf_settings.SESSION_COOKIE_HTTPONLY -SESSION_COOKIE_SAMESITE = osf_settings.SESSION_COOKIE_SAMESITE - -# csrf: -CSRF_COOKIE_NAME = 'api-csrf' -CSRF_COOKIE_SECURE = osf_settings.SECURE_MODE -CSRF_COOKIE_HTTPONLY = osf_settings.SECURE_MODE - -ALLOWED_HOSTS = [ - '.osf.io', -] - - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.staticfiles', - 'django.contrib.admin', - - # 3rd party - 'django_celery_beat', - 'django_celery_results', - 'rest_framework', - 'corsheaders', - 'raven.contrib.django.raven_compat', - 'django_extensions', - 'guardian', - 'storages', - 'waffle', - 'elasticsearch_metrics', - - # OSF - 'osf', - - # Addons - 'addons.osfstorage', - 'addons.bitbucket', - 'addons.box', - 'addons.dataverse', - 'addons.dropbox', - 'addons.figshare', - 'addons.forward', - 'addons.github', - 'addons.gitlab', - 'addons.googledrive', - 'addons.mendeley', - 'addons.onedrive', - 'addons.owncloud', - 'addons.s3', - 'addons.twofactor', - 'addons.wiki', - 'addons.zotero', - 'addons.swift', - 'addons.azureblobstorage', - 'addons.weko', - 'addons.jupyterhub', - 'addons.iqbrims', - 'addons.dropboxbusiness', - 'addons.nextcloudinstitutions', -) - -# local development using https -if osf_settings.SECURE_MODE and DEBUG: - INSTALLED_APPS += ('sslserver',) - -# TODO: Are there more granular ways to configure reporting specifically related to the API? -RAVEN_CONFIG = { - 'tags': {'App': 'api'}, - 'dsn': osf_settings.SENTRY_DSN, - 'release': osf_settings.VERSION, -} - -BULK_SETTINGS = { - 'DEFAULT_BULK_LIMIT': 100, -} - -MAX_PAGE_SIZE = 100 - -REST_FRAMEWORK = { - 'PAGE_SIZE': 10, - 'DEFAULT_RENDERER_CLASSES': ( - 'api.base.renderers.JSONAPIRenderer', - 'api.base.renderers.JSONRendererWithESISupport', - 'api.base.renderers.BrowsableAPIRendererNoForms', - ), - 'DEFAULT_PARSER_CLASSES': ( - 'api.base.parsers.JSONAPIParser', - 'api.base.parsers.JSONAPIParserForRegularJSON', - 'rest_framework.parsers.FormParser', - 'rest_framework.parsers.MultiPartParser', - ), - 'EXCEPTION_HANDLER': 'api.base.exceptions.json_api_exception_handler', - 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'api.base.content_negotiation.JSONAPIContentNegotiation', - 'DEFAULT_VERSIONING_CLASS': 'api.base.versioning.BaseVersioning', - 'DEFAULT_VERSION': '2.0', - 'ALLOWED_VERSIONS': ( - '2.0', - '2.1', - '2.2', - '2.3', - '2.4', - '2.5', - '2.6', - '2.7', - '2.8', - '2.9', - '2.10', - '2.11', - '2.12', - '2.13', - '2.14', - '2.15', - '2.16', - '2.17', - '2.18', - '2.19', - '2.20', - ), - 'DEFAULT_FILTER_BACKENDS': ('api.base.filters.OSFOrderingFilter',), - 'DEFAULT_PAGINATION_CLASS': 'api.base.pagination.JSONAPIPagination', - 'ORDERING_PARAM': 'sort', - 'DEFAULT_AUTHENTICATION_CLASSES': ( - # Custom auth classes - 'api.base.authentication.drf.OSFBasicAuthentication', - 'api.base.authentication.drf.OSFSessionAuthentication', - 'api.base.authentication.drf.OSFCASAuthentication', - ), - 'DEFAULT_THROTTLE_CLASSES': ( - 'rest_framework.throttling.UserRateThrottle', - 'api.base.throttling.NonCookieAuthThrottle', - 'api.base.throttling.BurstRateThrottle', - ), - 'DEFAULT_THROTTLE_RATES': { - 'user': '10000/day', - 'non-cookie-auth': '100/hour', - 'add-contributor': '10/second', - 'create-guid': '1000/hour', - 'root-anon-throttle': '1000/hour', - 'test-user': '2/hour', - 'test-anon': '1/hour', - 'send-email': '2/minute', - 'burst': '10/second', - }, -} - -# Settings related to CORS Headers addon: allow API to receive authenticated requests from OSF -# CORS plugin only matches based on "netloc" part of URL, so as workaround we add that to the list -CORS_ORIGIN_ALLOW_ALL = False -CORS_ORIGIN_WHITELIST = ( - urlparse(osf_settings.DOMAIN).netloc, - osf_settings.DOMAIN, -) -# This needs to remain True to allow cross origin requests that are in CORS_ORIGIN_WHITELIST to -# use cookies. -CORS_ALLOW_CREDENTIALS = True -# Set dynamically on app init -ORIGINS_WHITELIST = () - -MIDDLEWARE = ( - 'api.base.middleware.DjangoGlobalMiddleware', - 'api.base.middleware.CeleryTaskMiddleware', - 'api.base.middleware.PostcommitTaskMiddleware', - # A profiling middleware. ONLY FOR DEV USE - # Uncomment and add "prof" to url params to recieve a profile for that url - # 'api.base.middleware.ProfileMiddleware', - - # 'django.contrib.sessions.middleware.SessionMiddleware', - 'api.base.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - # 'django.contrib.auth.middleware.AuthenticationMiddleware', - # 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - # 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - # 'waffle.middleware.WaffleMiddleware', - 'api.base.middleware.SloanOverrideWaffleMiddleware', # Delete this and uncomment WaffleMiddleware to revert Sloan -) - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - }, -] - - -ROOT_URLCONF = 'api.base.urls' -WSGI_APPLICATION = 'api.base.wsgi.application' - - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# https://django-storages.readthedocs.io/en/latest/backends/gcloud.html -if os.environ.get('GOOGLE_APPLICATION_CREDENTIALS', False): - # Required to interact with Google Cloud Storage - DEFAULT_FILE_STORAGE = 'api.base.storage.RequestlessURLGoogleCloudStorage' - GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME', 'cos-osf-stage-cdn-us') - GS_FILE_OVERWRITE = os.environ.get('GS_FILE_OVERWRITE', False) -elif osf_settings.DEV_MODE or osf_settings.DEBUG_MODE: - DEFAULT_FILE_STORAGE = 'api.base.storage.DevFileSystemStorage' - -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_ROOT = os.path.join(BASE_DIR, 'static/vendor') - -API_BASE = 'v2/' -API_PRIVATE_BASE = '_/' -STATIC_URL = '/static/' - -NODE_CATEGORY_MAP = osf_settings.NODE_CATEGORY_MAP - -DEBUG_TRANSACTIONS = DEBUG - -JWT_SECRET = b'osf_api_cas_login_jwt_secret_32b' -JWE_SECRET = b'osf_api_cas_login_jwe_secret_32b' - -ENABLE_VARNISH = osf_settings.ENABLE_VARNISH -ENABLE_ESI = osf_settings.ENABLE_ESI -VARNISH_SERVERS = osf_settings.VARNISH_SERVERS -ESI_MEDIA_TYPES = osf_settings.ESI_MEDIA_TYPES - -ADDONS_FOLDER_CONFIGURABLE = ['box', 'dropbox', 's3', 'googledrive', 'figshare', 'owncloud', 'onedrive', 'swift', 'azureblobstorage', 'weko', 'iqbrims'] -ADDONS_OAUTH = ADDONS_FOLDER_CONFIGURABLE + ['dataverse', 'github', 'bitbucket', 'gitlab', 'mendeley', 'zotero', 'forward'] - -BYPASS_THROTTLE_TOKEN = 'test-token' - -OSF_SHELL_USER_IMPORTS = None - -# Settings for use in the admin -OSF_URL = 'https://osf.io' - -SELECT_FOR_UPDATE_ENABLED = True - -# Disable anonymous user permissions in django-guardian -ANONYMOUS_USER_NAME = None - -# If set to True, automated tests with extra queries will fail. -NPLUSONE_RAISE = False - -# Timestamp - number of requests to send to cloud storages per minute -TS_REQUESTS_PER_MIN = 30 - -# salt used for generating hashids -HASHIDS_SALT = 'pinkhimalayan' - -# django-elasticsearch-metrics -ELASTICSEARCH_DSL = { - 'default': { - 'hosts': os.environ.get('ELASTIC6_URI', '127.0.0.1:9201'), - 'retry_on_timeout': True, - }, -} -# Store yearly indices for time-series metrics -ELASTICSEARCH_METRICS_DATE_FORMAT = '%Y' - -WAFFLE_CACHE_NAME = 'waffle_cache' -STORAGE_USAGE_CACHE_NAME = 'storage_usage' - - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - }, - STORAGE_USAGE_CACHE_NAME: { - 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', - 'LOCATION': 'osf_cache_table', - }, - WAFFLE_CACHE_NAME: { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - }, -} - -SLOAN_ID_COOKIE_NAME = 'sloan_id' - -EGAP_PROVIDER_NAME = 'EGAP' - -MAX_SIZE_OF_ES_QUERY = 10000 -DEFAULT_ES_NULL_VALUE = 'N/A' - -TRAVIS_ENV = False - -### NII extensions -LOGIN_BY_EPPN = osf_settings.to_bool('LOGIN_BY_EPPN', False) -USER_TIMEZONE = osf_settings.USER_TIMEZONE -USER_LOCALE = osf_settings.USER_LOCALE -CLOUD_GATEWAY_ISMEMBEROF_PREFIX = osf_settings.CLOUD_GATEWAY_ISMEMBEROF_PREFIX -# install-addons.py -INSTALLED_APPS += ('addons.s3compat',) -ADDONS_FOLDER_CONFIGURABLE.append('s3compat') -ADDONS_OAUTH.append('s3compat') -INSTALLED_APPS += ('addons.myminio',) -ADDONS_FOLDER_CONFIGURABLE.append('myminio') -ADDONS_OAUTH.append('myminio') -INSTALLED_APPS += ('addons.nextcloud',) -ADDONS_FOLDER_CONFIGURABLE.append('nextcloud') -ADDONS_OAUTH.append('nextcloud') - -TST_COMMAND_DELIMITER = ' ' -# RSA key generation settings -SSL_GENERATE_KEY = 'openssl' + TST_COMMAND_DELIMITER + \ - 'genrsa' + TST_COMMAND_DELIMITER + \ - '-des3' + TST_COMMAND_DELIMITER + \ - '-out' + TST_COMMAND_DELIMITER + \ - '{0}.key {1}' -SSL_GENERATE_KEY_NOPASS = 'openssl' + TST_COMMAND_DELIMITER + \ - 'rsa' + TST_COMMAND_DELIMITER + \ - '-in' + TST_COMMAND_DELIMITER + \ - '{0}.key' + TST_COMMAND_DELIMITER + \ - '-out' + TST_COMMAND_DELIMITER + \ - '{0}.key.nopass' -SSL_GENERATE_CSR = 'openssl' + TST_COMMAND_DELIMITER + \ - 'req' + TST_COMMAND_DELIMITER + \ - '-new' + TST_COMMAND_DELIMITER + \ - '-key' + TST_COMMAND_DELIMITER + \ - '{0}.key.nopass' + TST_COMMAND_DELIMITER + \ - '-out' + TST_COMMAND_DELIMITER + \ - '{0}.csr' -SSL_GENERATE_SELF_SIGNED = 'openssl' + TST_COMMAND_DELIMITER + \ - 'req' + TST_COMMAND_DELIMITER + \ - '-x509' + TST_COMMAND_DELIMITER + \ - '-nodes' + TST_COMMAND_DELIMITER + \ - '-days' + TST_COMMAND_DELIMITER + \ - '365' + TST_COMMAND_DELIMITER + \ - '-newkey' + TST_COMMAND_DELIMITER + \ - 'rsa:2048' + TST_COMMAND_DELIMITER + \ - '-keyout' + TST_COMMAND_DELIMITER + \ - '{0}.key' + TST_COMMAND_DELIMITER + \ - '-out' + TST_COMMAND_DELIMITER + \ - '{0}.crt' -SSL_PRIVATE_KEY_GENERATION = 'openssl' + TST_COMMAND_DELIMITER + \ - 'genrsa' + TST_COMMAND_DELIMITER + \ - '-out' + TST_COMMAND_DELIMITER + \ - '{0}' + TST_COMMAND_DELIMITER + \ - '{1}' -SSL_PUBLIC_KEY_GENERATION = 'openssl' + TST_COMMAND_DELIMITER + \ - 'rsa' + TST_COMMAND_DELIMITER + \ - '-in' + TST_COMMAND_DELIMITER + \ - '{0}' + TST_COMMAND_DELIMITER + \ - '-pubout' + TST_COMMAND_DELIMITER + \ - '-out' + TST_COMMAND_DELIMITER + \ - '{1}' - -# UserKey Placement destination -KEY_NAME_PRIVATE = 'pvt' -KEY_NAME_PUBLIC = 'pub' -KEY_BIT_VALUE = '3072' -KEY_EXTENSION = '.pem' -KEY_SAVE_PATH = '/user_key_info/' -KEY_NAME_FORMAT = '{0}_{1}_{2}{3}' -PRIVATE_KEY_VALUE = 1 -PUBLIC_KEY_VALUE = 2 -# FreeTSA openation commands -SSL_CREATE_TIMESTAMP_REQUEST = 'openssl' + TST_COMMAND_DELIMITER + \ - 'ts' + TST_COMMAND_DELIMITER + \ - '-query' + TST_COMMAND_DELIMITER + \ - '-data' + TST_COMMAND_DELIMITER + \ - '{0}' + TST_COMMAND_DELIMITER + \ - '-cert' + TST_COMMAND_DELIMITER + \ - '-sha512' -SSL_GET_TIMESTAMP_RESPONSE = 'openssl' + TST_COMMAND_DELIMITER + \ - 'ts' + TST_COMMAND_DELIMITER + \ - '-verify' + TST_COMMAND_DELIMITER + \ - '-data' + TST_COMMAND_DELIMITER + \ - '{0}' + TST_COMMAND_DELIMITER + \ - '-in' + TST_COMMAND_DELIMITER + \ - '{1}' + TST_COMMAND_DELIMITER + \ - '-CAfile' + TST_COMMAND_DELIMITER + \ - '{2}' - -SSL_CREATE_TIMESTAMP_HASH_REQUEST = 'openssl ts -query -digest {digest} {digest_type} -cert' -SSL_GET_TIMESTAMP_HASH_RESPONSE = 'openssl ts -verify {digest_type} -digest {digest} -in {input} -CAfile {ca}' - -# openssl ts verify check value -OPENSSL_VERIFY_RESULT_OK = 'OK' -# timestamp verify rootKey -VERIFY_ROOT_CERTIFICATE = 'root_cert_verifycate.pem' -# timestamp request const -REQUEST_HEADER = {'Content-Type': 'application/timestamp-query'} -TIME_STAMP_AUTHORITY_URL = 'http://eswg.jnsa.org/freetsa' -ERROR_HTTP_STATUS = [400, 401, 402, 403, 500, 502, 503, 504] -REQUEST_TIME_OUT = 5 -RETRY_COUNT = 3 - -# UPKI flag -USE_UPKI = False - -#uPKI operation commands -UPKI_TIMESTAMP_URL = '' -UPKI_CREATE_TIMESTAMP = '' # {0}=target, {1}=out -UPKI_VERIFY_TIMESTAMP = '' # {0}=target, {1}=in -UPKI_CREATE_TIMESTAMP_HASH = '' # {digest_type}, {digest}, {output} -UPKI_VERIFY_TIMESTAMP_HASH = '' # {digest}, {input} -UPKI_VERIFY_INVALID_MSG = 'LPC_ERR_VERIFY_INVALID' - -# TimeStamp Inspection Status -TIME_STAMP_TOKEN_UNCHECKED = 0 -TIME_STAMP_TOKEN_CHECK_SUCCESS = 1 -TIME_STAMP_TOKEN_CHECK_SUCCESS_MSG = 'OK' -TIME_STAMP_TOKEN_CHECK_NG = 2 -TIME_STAMP_TOKEN_CHECK_NG_MSG = 'Fail: file modified.' -TIME_STAMP_TOKEN_CHECK_FILE_NOT_FOUND = 3 -TIME_STAMP_TOKEN_CHECK_FILE_NOT_FOUND_MSG = 'Fail: not inspected.' -TIME_STAMP_TOKEN_NO_DATA = 4 -TIME_STAMP_TOKEN_NO_DATA_MSG = 'Error: some errors has occurred in processing.' -FILE_NOT_EXISTS = 5 -FILE_NOT_EXISTS_MSG = 'Fail: deleted file.' -FILE_NOT_FOUND = 6 -FILE_NOT_FOUND_MSG = 'Fail: file was gone.' -TIME_STAMP_VERIFICATION_ERR = 7 -TIME_STAMP_VERIFICATION_ERR_MSG = 'Error: some errors has occurred in verification.' -TIME_STAMP_STORAGE_DISCONNECTED = 8 -TIME_STAMP_STORAGE_DISCONNECTED_MSG = 'Error: storage disconnected.' -TIME_STAMP_STORAGE_NOT_ACCESSIBLE = 9 -TIME_STAMP_STORAGE_NOT_ACCESSIBLE_MSG = 'Error: storage service connection error occurred.' - -# Quota settings -DEFAULT_MAX_QUOTA = 100 -WARNING_THRESHOLD = 0.9 -BASE_FOR_METRIC_PREFIX = 1000 -SIZE_UNIT_GB = BASE_FOR_METRIC_PREFIX ** 3 -NII_STORAGE_REGION_ID = 1 diff --git a/StorageAddon/osf.io/config/framework/addons/data/addons.json b/StorageAddon/osf.io/config/framework/addons/data/addons.json deleted file mode 100644 index 2a80564..0000000 --- a/StorageAddon/osf.io/config/framework/addons/data/addons.json +++ /dev/null @@ -1,628 +0,0 @@ -{ - "addons": { - "GitHub": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making a GitHub repo public or private. The GakuNin RDM does not alter the permissions of linked GitHub repos." - }, - "View / download file versions": { - "status": "full", - "text": "GitHub files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in GitHub." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in GitHub." - }, - "Logs": { - "status": "full", - "text": "GitHub dynamically updates GakuNin RDM logs when files are modified outside the GakuNin RDM. Changes to GitHub repos made before the repo is linked to the GakuNin RDM will not be reflected in GakuNin RDM logs." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Github authorization unless the user forking the project is the same user who authorized the Github add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "GitHub content will be registered, but version history will not be copied to the registration." - } - }, - "GitLab": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making a GitLab repo public or private. The GakuNin RDM does not alter the permissions of linked GitLab repos." - }, - "View / download file versions": { - "status": "full", - "text": "GitLab files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "none", - "text": "Adding/updating files in the project via GakuNin RDM is not implemented yet." - }, - "Delete files": { - "status": "none", - "text": "Deleting files via GakuNin RDM is not implemented yet." - }, - "Logs": { - "status": "none", - "text": "GakuNin RDM does not keep track of changes made using Gitlab directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy GitLab authorization unless the user forking the project is the same user who authorized the GitLab add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "GitLab content will be registered, but version history will not be copied to the registration." - } - }, - "Amazon S3": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making an S3 bucket public or private. The GakuNin RDM does not alter the permissions of linked S3 buckets." - }, - "View / download file versions": { - "status": "partial", - "text": "The S3 add-on supports file versions if versioning is enabled for your S3 buckets." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Amazon S3." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Amazon S3." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your S3 buckets through the GakuNin RDM, but not for changes made using S3 directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy S3 authorization unless the user forking the project is the same user who authorized the S3 add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "S3 content will be registered, but version history will not be copied to the registration." - } - }, - "figshare": { - "Permissions": { - "status": "partial", - "text": " Making a GakuNin RDM project public or private is independent of making figshare content public or private. The GakuNin RDM does not alter the permissions of linked figshare content." - }, - "View / download file versions": { - "status": "partial", - "text": "figshare files can be viewed/downloaded via GakuNin RDM, but version history is not supported." - }, - "Add / update files": { - "status": "partial", - "text": "Files can be added but not updated." - }, - "Delete files": { - "status": "partial", - "text": "Private files and filesets can be deleted via GakuNin RDM." - }, - "Logs": { - "status": "partial", - "text": "GakuNin RDM keeps track of changes you make to your figshare content through GakuNin RDM, but not for changes made using figshare directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy figshare authorization unless the user forking the project is the same user who authorized the figshare add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "figshare content will be registered, but version history will not be copied to the registration." - } - }, - "Dropbox": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of Dropbox privacy. The GakuNin RDM does not alter the permissions of linked Dropbox folders." - }, - "View / download file versions": { - "status": "full", - "text": "Dropbox files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Dropbox." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Dropbox." - }, - "Logs": { - "status": "partial", - "text": "GakuNin RDM keeps track of changes you make to your Dropbox content through GakuNin RDM, but not for changes made using Dropbox directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Dropbox authorization unless the user forking the project is the same user who authorized the Dropbox add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "Dropbox content will be registered, but version history will not be copied to the registration." - } - }, - "OneDrive": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of OneDrive privacy. The GakuNin RDM does not alter the permissions of linked OneDrive folders." - }, - "View / download file versions": { - "status": "full", - "text": "OneDrive files and their versions can be viewed/downloaded via GakuNin RDM. OneNote files are unexportable and cannot be downloaded or viewed." - }, - "Add / update files": { - "status": "none", - "text": "The OneDrive add-on is read-only." - }, - "Delete files": { - "status": "none", - "text": "The OneDrive add-on is read-only." - }, - "Logs": { - "status": "partial", - "text": "GakuNin RDM keeps track of changes you make to your OneDrive add-on configuration. It does not track changes to OneDrive content." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy OneDrive authorization unless the user forking the project is the same user who authorized the OneDrive add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "OneDrive content will be registered, but version history will not be copied to the registration. OneNote files are unexportable and will not be archived." - } - }, - "Dataverse": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making a Dataverse study public or private. The GakuNin RDM allows you to release the latest draft version of a Dataverse dataset." - }, - "View / download file versions": { - "status": "partial", - "text": "Files from the latest release of the selected Dataverse study can be viewed/downloaded. GakuNin RDM users with write permissions can view/download draft files as well." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Dataverse." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Dataverse." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your Dataverse studies through the GakuNin RDM, but not for changes made using Dataverse directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Dataverse authorization unless the user forking the project is the same user who authorized the Dataverse add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "Dataverse content will be registered, but version history will not be copied to the registration." - } - }, - "Google Drive": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of Google Drive privacy. The GakuNin RDM does not alter the permissions of linked Google Drive folders." - }, - "View / download file versions": { - "status": "full", - "text": "Google Drive files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Google Drive." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Google Drive." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your Google Drive content through the GakuNin RDM, but not for changes made using Google Drive directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Google Drive authorization unless the user forking the project is the same user who authorized the Google Drive add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "Google Drive content will be registered, but version history will not be copied to the registration." - } - }, - "Box": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of Box privacy. The GakuNin RDM does not alter the permissions of linked Box folders." - }, - "View / download file versions": { - "status": "full", - "text": "Box files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Box." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Box." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your Box content through the GakuNin RDM, but not for changes made using Box directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Box authorization unless the user forking the project is the same user who authorized the Box add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "Box content will be registered, but version history will not be copied to the registration." - } - }, - "Mendeley": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making a Mendeley folder public or private. The GakuNin RDM does not alter the permissions of a linked Mendeley folder." - }, - "View / download file versions": { - "status": "NA" - }, - "Add / update files": { - "status": "NA" - }, - "Delete files": { - "status": "NA" - }, - "Logs": { - "status": "NA" - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Mendeley authorization unless the user forking the project is the same user who authorized the Mendeley add-on in the source project being forked." - }, - "Registering": { - "status": "none", - "text": "Mendeley content will not be registered." - } - }, - "ownCloud": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of ownCloud privacy. The GakuNin RDM does not alter the permissions of linked ownCloud folders." - }, - "View / download file versions": { - "status": "partial", - "text": "ownCloud files can be viewed/downloaded via GakuNin RDM, but version history is not supported by the ownCloud WebDAV API." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in ownCloud." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in ownCloud." - }, - "Logs": { - "status": "partial", - "text": "GakuNin RDM keeps track of changes you make to your ownCloud content through GakuNin RDM, but not for changes made using ownCloud directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy ownCloud authorization unless the user forking the project is the same user who authorized the ownCloud add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "ownCloud content will be registered, but version history will not be copied to the registration." - } - }, - "Zotero": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making a Zotero folder public or private. The GakuNin RDM does not alter the permissions of a linked Zotero folder." - }, - "View / download file versions": { - "status": "NA" - }, - "Add / update files": { - "status": "NA" - }, - "Delete files": { - "status": "NA" - }, - "Logs": { - "status": "NA" - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Zotero authorization unless the user forking the project is the same user who authorized the Zotero add-on in the source project being forked." - }, - "Registering": { - "status": "none", - "text": "Zotero content will not be registered." - } - }, - "Bitbucket": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making a Bitbucket repo public or private. The GakuNin RDM does not alter the permissions of linked Bitbucket repos." - }, - "View / download file versions": { - "status": "full", - "text": "Bitbucket files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "none", - "text": "Bitbucket does not support adding or updating files via their API." - }, - "Delete files": { - "status": "none", - "text": "Bitbucket does not support deleting files via their API." - }, - "Logs": { - "status": "full", - "text": "GakuNin RDM keeps track of changes you make to your Bitbucket content through GakuNin RDM, but not for changes made using Bitbucket directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Bitbucket authorization unless the user forking the project is the same user who authorized the Bitbucket add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "Bitbucket content will be registered, but version history will not be copied to the registration." - } - }, - "OpenStack Swift": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making an Swift container public or private." - }, - "View / download file versions": { - "status": "partial", - "text": "The Swift add-on does not support file versions." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Swift." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Swift." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your Swift containers through the GakuNin RDM, but not for changes made using Swift directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Swift authorization unless the user forking the project is the same user who authorized the Swift add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "Swift object will be registered, but version history will not be copied to the registration." - } - }, - "Azure Blob Storage": { - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making an Azure Blob Storage container public or private." - }, - "View / download file versions": { - "status": "partial", - "text": "The Azure Blob Storage add-on supports only Block Blobs, and does not support file versions." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Azure Blob Storage." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Azure Blob Storage." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your Azure Blob Storage containers through the GakuNin RDM, but not for changes made using Azure Blob Storage directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Azure Blob Storage authorization unless the user forking the project is the same user who authorized the Azure Blob Storage add-on in the source project being forked." - }, - "Registering": { - "status": "none", - "text": "Azure Blob Storage content will not be registered." - } - }, - "WEKO": { - "Permissions": { - "status": "NA" - }, - "View / download file versions": { - "status": "NA" - }, - "Add / update files": { - "status": "partial", - "text": "Adding/updating indices/items in the project via GakuNin RDM will be reflected in WEKO." - }, - "Delete files": { - "status": "partial", - "text": "Items deleted via GakuNin RDM will be deleted in WEKO." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your WEKO index through the GakuNin RDM, but not for changes made using WEKO directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy WEKO authorization unless the user forking the project is the same user who authorized the WEKO add-on in the source project being forked." - }, - "Registering": { - "status": "none", - "text": "WEKO content will not be registered." - } - }, - "S3 Compatible Storage": { - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy S3 Compatible Storage authorization unless the user forking the project is the same user who authorized the S3 Compatible Storage add-on in the source project being forked." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your S3 Compatible Storage buckets through the GakuNin RDM, but not for changes made using S3 Compatible Storage directly." - }, - "Registering": { - "status": "partial", - "text": "S3 Compatible Storage content will be registered, but version history will not be copied to the registration." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in S3 Compatible Storage." - }, - "View / download file versions": { - "status": "partial", - "text": "The S3 add-on supports file versions if versioning is enabled for your S3 Compatible Storage buckets." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in S3 Compatible Storage." - }, - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making an S3 Compatible Storage bucket public or private. The GakuNin RDM does not alter the permissions of linked S3 Compatible Storage buckets." - } - }, - "My MinIO": { - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy My MinIO authorization unless the user forking the project is the same user who authorized the My MinIO add-on in the source project being forked." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your My MinIO buckets through the GakuNin RDM, but not for changes made using My MinIO directly." - }, - "Registering": { - "status": "partial", - "text": "My MinIO content will be registered, but version history will not be copied to the registration." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in My MinIO." - }, - "View / download file versions": { - "status": "partial", - "text": "The S3 add-on supports file versions if versioning is enabled for your My MinIO buckets." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in My MinIO." - }, - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of making an My MinIO bucket public or private. The GakuNin RDM does not alter the permissions of linked My MinIO buckets." - } - }, - "JupyterHub": { - "Permissions": { - "status": "none", - "text": "The GakuNin RDM does not affect the permissions of JupyterHub." - }, - "View / download file versions": { - "status": "none", - "text": "The JupyterHub add-on does not provide Storage Features." - }, - "Add / update files": { - "status": "none", - "text": "The JupyterHub add-on does not provide Storage Features." - }, - "Delete files": { - "status": "none", - "text": "The JupyterHub add-on does not provide Storage Features." - }, - "Logs": { - "status": "none", - "text": "The JupyterHub add-on does not provide Storage Features." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component copies information about linked JupyterHub but the GakuNin RDM does not affect authentication of JupyterHub." - }, - "Registering": { - "status": "none", - "text": "JupyterHub information will not be registered." - } - }, - "Nextcloud": { - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Nextcloud authorization unless the user forking the project is the same user who authorized the Nextcloud add-on in the source project being forked." - }, - "Logs": { - "status": "partial", - "text": "GakuNin RDM keeps track of changes you make to your Nextcloud content through GakuNin RDM, but not for changes made using Nextcloud directly." - }, - "Registering": { - "status": "partial", - "text": "Nextcloud content will be registered, but version history will not be copied to the registration." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Nextcloud." - }, - "View / download file versions": { - "status": "partial", - "text": "Nextcloud files can be viewed/downloaded via GakuNin RDM, but version history is not supported by the Nextcloud WebDAV API." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Nextcloud." - }, - "Permissions": { - "status": "partial", - "text": "Making a GakuNin RDM project public or private is independent of Nextcloud privacy. The GakuNin RDM does not alter the permissions of linked Nextcloud folders." - } - }, - "IQB-RIMS": { - "Permissions": { - "status": "partial", - "text": "Making an GakuNin RDM project public or private is independent of Google Drive privacy. The GakuNin RDM does not alter the permissions of linked Google Drive folders." - }, - "View / download file versions": { - "status": "full", - "text": "IQB-RIMS files and their versions can be viewed/downloaded via GakuNin RDM." - }, - "Add / update files": { - "status": "full", - "text": "Adding/updating files in the project via GakuNin RDM will be reflected in Google Drive." - }, - "Delete files": { - "status": "full", - "text": "Files deleted via GakuNin RDM will be deleted in Google Drive." - }, - "Logs": { - "status": "partial", - "text": "The GakuNin RDM keeps track of changes you make to your Google Drive content through the GakuNin RDM, but not for changes made using Google Drive directly." - }, - "Forking": { - "status": "partial", - "text": "Forking a project or component does not copy Google Drive authorization unless the user forking the project is the same user who authorized the IQB-RIMS add-on in the source project being forked." - }, - "Registering": { - "status": "partial", - "text": "IQB-RIMS content will be registered, but version history will not be copied to the registration." - } - } - }, - "disclaimers": [ - "This add-on connects your GakuNin RDM project to an external service. Use of this service is bound by its terms and conditions. The GakuNin RDM is not responsible for the service or for your use thereof.", - "This add-on allows you to store files using an external service. Files added to this add-on are not stored within the GakuNin RDM." - ] -} diff --git a/StorageAddon/osf.io/config/website/static/storageAddons.json b/StorageAddon/osf.io/config/website/static/storageAddons.json deleted file mode 100644 index 02c7156..0000000 --- a/StorageAddon/osf.io/config/website/static/storageAddons.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "bitbucket": { - "fullName": "Bitbucket", - "externalView": true - }, - "box": { - "fullName": "Box", - "externalView": false - }, - "dataverse": { - "fullName": "Dataverse", - "externalView": false - }, - "dropbox": { - "fullName": "Dropbox", - "externalView": false - }, - "figshare": { - "fullName": "figshare", - "externalView": true - }, - "github": { - "fullName": "GitHub", - "externalView": true - }, - "gitlab": { - "fullName": "GitLab", - "externalView": true - }, - "googledrive": { - "fullName": "Google Drive", - "externalView": true - }, - "osfstorage": { - "fullName": "NII Storage", - "externalView": false - }, - "owncloud": { - "fullName": "ownCloud", - "externalView": false - }, - "s3": { - "fullName": "Amazon S3", - "externalView": false - }, - "onedrive": { - "fullName": "OneDrive", - "externalView": true - }, - "swift": { - "fullName": "OpenStack Swift", - "externalView": false - }, - "azureblobstorage": { - "fullName": "Azure Blob Storage", - "externalView": false - }, - "s3compat": { - "fullName": "S3 Compatible Storage", - "externalView": false - }, - "myminio": { - "fullName": "My MinIO", - "externalView": false - }, - "weko": { - "fullName": "WEKO", - "externalView": false - }, - "nextcloud": { - "fullName": "Nextcloud", - "externalView": false - }, - "iqbrims": { - "fullName": "IQB-RIMS", - "externalView": false - }, - "nextcloudinstitutions": { - "fullName": "Nextcloud for Institutions", - "externalView": false - }, - "dropboxbusiness": { - "fullName": "Dropbox Business", - "externalView": false - } -} diff --git a/StorageAddon/osf.io/config/website/translations/ja/LC_MESSAGES/js_messages.po b/StorageAddon/osf.io/config/website/translations/ja/LC_MESSAGES/js_messages.po deleted file mode 100644 index 7c0b988..0000000 --- a/StorageAddon/osf.io/config/website/translations/ja/LC_MESSAGES/js_messages.po +++ /dev/null @@ -1,8982 +0,0 @@ -# Japanese translations for . -# Copyright (C) 2020 ORGANIZATION -# This file is distributed under the same license as the project. -# FIRST AUTHOR , 2020. -# -msgid "" -msgstr "" -"Project-Id-Version: 0.6.0\n" -"Report-Msgid-Bugs-To: rocs-office@nii.ac.jp\n" -"POT-Creation-Date: 2021-03-13 05:48+0000\n" -"PO-Revision-Date: 2020-09-03 12:45+0900\n" -"Last-Translator: Shunta Nishikawa\n" -"Language: ja\n" -"Language-Team: RCOS \n" -"Plural-Forms: nplurals=1; plural=0\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.5.1\n" - -#: addons/twofactor/static/twoFactorUserConfig.js:148 -#: addons/twofactor/static/twoFactorUserConfig.js:190 -#: admin/static/js/rdm_addons/rdm-addons-page.js:71 -#: website/static/js/accountSettings.js:286 -#: website/static/js/accountSettings.js:351 -#: website/static/js/accountSettings.js:477 -#: website/static/js/accountSettings.js:549 -#: website/static/js/addProjectPlugin.js:347 -#: website/static/js/addonPermissions.js:55 -#: website/static/js/addonSettings.js:71 website/static/js/addonSettings.js:134 -#: website/static/js/apiApplication.js:249 -#: website/static/js/apiApplication.js:420 -#: website/static/js/apiApplication.js:487 -#: website/static/js/apiPersonalToken.js:240 -#: website/static/js/apiPersonalToken.js:411 -#: website/static/js/apiPersonalToken.js:438 -#: website/static/js/citationsNodeConfig.js:169 -#: website/static/js/citationsNodeConfig.js:187 -#: website/static/js/contribManager.js:433 website/static/js/fangorn.js:546 -#: website/static/js/fangorn.js:551 website/static/js/fangorn.js:1215 -#: website/static/js/fangorn.js:1238 website/static/js/fangorn.js:1291 -#: website/static/js/fangorn.js:1310 website/static/js/fangorn.js:2009 -#: website/static/js/filepage/index.js:208 -#: website/static/js/folderPickerNodeConfig.js:510 -#: website/static/js/folderPickerNodeConfig.js:566 -#: website/static/js/institutionProjectSettings.js:102 -#: website/static/js/licensePicker.js:243 website/static/js/myProjects.js:1560 -#: website/static/js/myProjects.js:1608 website/static/js/myProjects.js:1632 -#: website/static/js/nodeControl.js:227 -#: website/static/js/oauthAddonNodeConfig.js:156 -#: website/static/js/oauthAddonNodeConfig.js:174 -#: website/static/js/osfHelpers.js:796 website/static/js/osfHelpers.js:897 -#: website/static/js/pages/profile-settings-addons-page.js:35 -#: website/static/js/pages/profile-settings-addons-page.js:101 -#: website/static/js/pages/project-addons-page.js:41 -#: website/static/js/pages/project-addons-page.js:66 -#: website/static/js/privateLinkTable.js:119 website/static/js/profile.js:373 -#: website/static/js/profile.js:743 website/static/js/profile.js:975 -#: website/static/js/project.js:39 website/static/js/project.js:143 -#: website/static/js/project.js:253 -#: website/static/js/wikiSettingsTreebeard.js:31 -msgid "Cancel" -msgstr "キャンセル" - -#: addons/twofactor/static/twoFactorUserConfig.js:178 -msgid "Enable Two-factor Authentication" -msgstr "2要素認証を有効にする" - -#: addons/twofactor/static/twoFactorUserConfig.js:179 -msgid "" -"Enabling two-factor authentication will not immediately activate this " -"feature for your account. You will need to follow the steps that appear " -"below to complete the activation of two-factor authentication for your " -"account." -msgstr "" -"2要素認証を有効にしても、アカウントのこの機能はすぐには有効になりません。 " -"アカウントの2要素認証のアクティベーションを完了するには、以下に表示される手順に従う必要があります。" - -#: addons/twofactor/static/twoFactorUserConfig.js:187 -msgid "Enable" -msgstr "有効にする" - -#: addons/wiki/static/WikiEditor.js:86 -msgid "Live editing mode" -msgstr "ライブ編集モード" - -#: addons/wiki/static/WikiEditor.js:88 -msgid "Attempting to connect" -msgstr "接続試行中" - -#: addons/wiki/static/WikiEditor.js:90 -msgid "Unsupported browser" -msgstr "サポートされていないブラウザ" - -#: addons/wiki/static/WikiEditor.js:92 -msgid "Unavailable: Live editing" -msgstr "利用不可:ライブ編集" - -#: addons/wiki/static/WikiEditor.js:153 -msgid "The wiki content could not be loaded." -msgstr "Wikiコンテンツをロードできませんでした。" - -#: addons/wiki/static/WikiEditor.js:155 -msgid "Could not GET wiki contents." -msgstr "Wikiコンテンツを取得できませんでした。" - -#: addons/wiki/static/WikiEditor.js:176 -msgid "There are unsaved changes to your wiki. If you exit " -msgstr "wikiに未保存の変更があります。" - -#: addons/wiki/static/WikiEditor.js:177 -msgid "the page now, those changes may be lost." -msgstr "ここでページを終了すると、これらの変更が失われる可能性があります。" - -#: addons/wiki/static/wikiPage.js:94 -#: website/static/js/pages/project-dashboard-page.js:733 -msgid "" -"*Add important information, links, or images here to describe your " -"project.*" -msgstr "*重要な情報、リンク、または画像をここに追加して、プロジェクトを説明してください。*" - -#: addons/wiki/static/wikiPage.js:96 -#: website/static/js/pages/project-dashboard-page.js:735 -msgid "*No wiki content.*" -msgstr "* Wikiコンテンツはありません。*" - -#: addons/wiki/static/wikiPage.js:220 -msgid "Live preview" -msgstr "ライブプレビュー" - -#: addons/wiki/static/wikiPage.js:222 -msgid "Current version" -msgstr "現在のバージョン" - -#: addons/wiki/static/wikiPage.js:224 -msgid "Previous version" -msgstr "前のバージョン" - -#: addons/wiki/static/wikiPage.js:226 -msgid "Version " -msgstr "バージョン" - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:48 -msgid "Strong " -msgstr "強調構文 " - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:51 -msgid "Emphasis " -msgstr "強調構文 " - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:54 -msgid "Spellcheck: Toggle spellcheck on and off" -msgstr "スペルチェック:スペルチェックのオンとオフを切り替えます" - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:56 -msgid "Hyperlink " -msgstr "ハイパーリンク " - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:60 -msgid "Blockquote
" -msgstr "引用構文
" - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:63 -msgid "Code Sample
"
-msgstr "コードサンプル 
"
-
-#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:66
-msgid "Image "
-msgstr "画像 "
-
-#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:70
-msgid "Numbered List 
    " -msgstr "段落番号
      " - -#: addons/wiki/static/pagedown-ace/Markdown.Editor.js:71 -msgid "Bulleted List