Visual Studio 2015 の UnitTest で Oracle.DataAccess が読み込めない
Visual Studio 2015 で Oracle.DataAccess を使った UnitTest を実行すると
System.BadImageFormatException: System.BadImageFormatException: ファイルまたはアセンブリ 'Oracle.DataAccess, Version=4.112.4.0, Culture=neutral, PublicKeyToken=89b483f429c47342'、またはその依存関係の 1 つが読み込めませんでした。間違ったフォーマットのプログラムを読み込もうとしました。
といった、エラーが発生してテストが実行できなかったが、意外なところに設定があった。
「既定のプロセッサ アーキテクチャ」を「X64」に変更すると動作するようになる。
OS も 64bit、Oracle.DataAccess.dll も 64bit なので当たり前といえば当たり前なのですが、設定箇所がわからずにずいぶん探しました。
Knowledge→GROWI 移行 (7)
移行実施
いよいよ、knowledge から GROWI にデータを移行します。移行に際して、下記のような移行用のプログラムを作成しました。使い捨てです。
import psycopg2 from psycopg2.extras import DictCursor from growiclient import GrowiClient # DB接続情報 DB_HOST = 'postgres' DB_PORT = '5432' DB_NAME = 'knowledgedb' DB_USER = 'kbadmin' DB_PASS = '**********' # GROWI 接続情報 GROWI_HOST = 'growi' GROWI_PORT = '3000' GROWI_SSL = False GROWI_API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' GROWI_USER = 'tiger' # DB接続関数 def get_connection(): return psycopg2.connect('postgresql://{user}:{password}@{host}:{port}/{dbname}' .format(user=DB_USER, password=DB_PASS, host=DB_HOST, port=DB_PORT, dbname=DB_NAME)) # 添付ファイルの移行 def migrate_attachments(conn, growi_client, page, id): with conn.cursor(name='files_cursor', cursor_factory=DictCursor) as file_cur: id_col = 'knowledge_id' if growi_client.is_draft(): id_col = 'draft_id' file_cur.execute('select file_no, knowledge_id, comment_no, draft_id, file_name, file_binary ' 'from knowledge_files ' 'where {} = %s'.format(id_col), (id, )) for file_row in file_cur: print(file_row['file_name']) file_path = './knowledge_to_growi/attachments/' + file_row['file_name'] with open(file_path, 'wb') as f: f.write(file_row['file_binary']) file_url = growi_client.set_attachment(page, file_path) page.replace_attachment(file_row['file_name'], file_url) # Knowledge -> GROWI ページ移行 def migrage_knowledge(conn, growi_client): with conn.cursor(name='knowledges_cursor', cursor_factory=DictCursor) as kb_cur: if growi_client.is_draft(): kb_cur.execute('select draft_id, tag_names, title, content from draft_knowledges ' 'where delete_flag = 0 ' ' and (knowledge_id is null or knowledge_id not in (select knowledge_id from knowledges)) ' 'order by draft_id') else: kb_cur.execute('select knowledge_id, tag_names, title, content ' 'from knowledges ' 'where delete_flag = 0 ' 'order by knowledge_id') titles = {} for kb_row in kb_cur: if growi_client.is_draft(): id = kb_row['draft_id'] else: id = kb_row['knowledge_id'] print(str(id) + ' : ' + kb_row['title']) page_tags = [] if kb_row['tag_names']: page_tags = kb_row['tag_names'].replace(chr(0xa0), '').split(',') if len(page_tags) == 1: page_tags.append('') title = kb_row['title'] if title in titles: titles[title] += 1 title += '({})'.format(str(titles[title])) else: titles[title] = 1 content = kb_row['content'] if len(content) == 0: content = "## {}".format(title) page = growi_client.create_page(title, page_tags, content) migrate_attachments(conn, growi_client, page, id) growi_client.update_page(page) with get_connection() as conn: # 並び順を後ろの方にするためにドラフトページを先に移行 growi_client = GrowiClient( GROWI_HOST, GROWI_PORT, GROWI_API_KEY, GROWI_USER, GROWI_SSL, True) migrage_knowledge(conn, growi_client) # 公開ページの移行 growi_client = GrowiClient( GROWI_HOST, GROWI_PORT, GROWI_API_KEY, GROWI_USER, GROWI_SSL) migrage_knowledge(conn, growi_client)
実行して、簡単に内容を確認してみましたが大丈夫そうです。
移行元のデータベースは Docker コンテナの状態で保管しておくので、問題が見つかれば、その時に対応することにします。
実はもっと簡単に済むんじゃないかと思っていたのですが、意外に時間がかかりました。しかし、Python から使える GROWI のクライアントを手に入れたので、今後何かに使えるんじゃないかと思っています。
Knowledge→GROWI 移行 (6)
移行用の GROWI アクセス クラスを作る
GROWI API の使い方もだいたいわかってきたので、Knowledge→GROWI データ移行のプログラムは書けると思います。データの移行さえしてしまえばいいので、ダラダラと書いてもいいと思いましたが、Python の学習も兼ねて GROWI にアクセスするためのクラスを作ることにしました。
で、作ったのがこれです。
import os import requests import json import mimetypes import re class GrowiClient: """ GROWI クライアント """ def __init__(self, growihost, port, apitoken, username, ssl=False, draft=False): """ Parameters ---------- growihost : str GROWI ホスト名 apitoken : str GROWI API Token username : str GROWI ユーザ名 ssl : bool true : Yes false : No draft : bool true ドラフト false 公開 """ self.base_url = 'http{}://{}'.format('s' if ssl else '', growihost) if port: self.base_url += ':{}'.format(port) self.base_url += '/_api' self.base_path = '/{}'.format(username) self.params = {"access_token": apitoken, "user": username} self.draft = draft self.cur_pages = {} growi_res = self.__get('pages.list', {"limit": -1}) for page in growi_res['pages']: self.cur_pages[page['path']] = GrowiPage(page['_id'], page['path'], page['revision'], None, None, None) def create_page(self, title, tags, content): """ GROWI のベージを作製する Parameters ---------- title : str ページ・タイトル tags : array タグ content : str ページ本文 Retruns ------- page : GrowiPage GROWI のページを表すオブジェクト """ path = self.__to_path(title) if path in self.cur_pages: page = self.cur_pages[path] page.title = title page.tags = tags page.content = content self.__initialize_attachments_info(page) self.update_page(page) return page payload = {"body": content, "path": path} res = self.__post('v3/pages', payload) self.cur_pages[path] = GrowiPage(res['page']['id'], res['page']['path'], res['page']['revision'], title, tags, content) return self.cur_pages[path] def set_attachment(self, page, file_path): """ 指定されたファイルを指定された GROWI ページの添付ファイルとして設定する Parameters ---------- page : GrowiPage 移行対象の GROWI ページを表すオブジェクト file_path : str 移行するファイルのパス Retruns ------- file_url : 追加した添付ファイルの参照 url """ file_name = os.path.basename(file_path) attachment_info = page.get_attachment_info(file_name) if attachment_info: self.__remove_attachment(page, attachment_info) mime_type = mimetypes.guess_type(file_name)[0] file = {'file': (file_name, open(file_path, 'rb'), mime_type)} payload = {"page_id": page.id, "path": page.path} res = self.__post('attachments.add', payload, file) page.add_attachment_info(res['attachment']['id'], res['attachment']['originalName'], res['attachment']['filePathProxied']) return res['attachment']['filePathProxied'] def update_page(self, page): """ GROWI ページを更新する Parameters ---------- page : GrowiPage GROWI ページを表すオブジェクト """ payload = {"body": page.content, "pageTags": page.tags, "page_id": page.id, "revision_id": page.revision} res = self.__post('pages.update', payload) page.revision = res['page']['revision'] def is_draft(self): """ DRAFT ページに対する処理かどうかを返す Retruns ------- draft : bool True Yes False No """ return self.draft def __initialize_attachments_info(self, page): """ 指定された GROWI ページ情報の添付ファイル情報を初期化する Parameters ---------- page : GrowiPage GROWI ページを表すオブジェクト """ page_no = 1 while True: growi_res = self.__get('v3/attachment/list', {"pageId": page.id, "page": page_no}) if len(growi_res['paginateResult']['docs']) == 0: break page.initialize_attachments_info(growi_res) page_no += 1 def __remove_attachment(self, page, attachment_info): """ 添付ファイルを削除する Parameters ---------- page : GrowiPage GROWI ページを表すオブジェクト attachment_info : GrowiAttachment 削除する添付ファイル情報 """ print("Remove attachment : {}".format(attachment_info.original_name)) payload = {"attachment_id": attachment_info.id} self.__post('attachments.remove', payload) page.remove_attachment_info(attachment_info.id) def __post(self, verb, payload, file=None): """ GROWI サーバーに POST リクエストを行う Parameters ---------- verb : str GROWI API payload : dict リクエストボディ file : dict アップロードファイル情報 {'name': ('filename', fileobj)} Retruns ------- growi_res : json リクエストのレスポンス """ url = self.base_url + '/{}'.format(verb) res = requests.post(url, data=payload, files=file, params=self.params) res.raise_for_status growi_res = res.json() # print(json.dumps(growi_res, indent=4)) if 'errors' in growi_res: print(json.dumps(growi_res, indent=4)) return growi_res def __get(self, verb, params=None): """ GROWI サーバーに GET リクエストを行う Parameters ---------- verb : str GROWI API params : dict GET のパラメタ Retruns ------- growi_res : json リクエストのレスポンス """ url = self.base_url + '/{}'.format(verb) req_params = self.params.copy() if params: req_params.update(params) res = requests.get(url, params=req_params) res.raise_for_status growi_res = res.json() # print(json.dumps(growi_res, indent=4)) if 'errors' in growi_res: print(json.dumps(growi_res, indent=4)) return growi_res def __to_path(self, title): """ GROWI ページのバスを返す Parameters ---------- title : str ページ・タイトル Retruns ------- path : str GROWI のページのバス """ path = '{}/'.format(self.base_path) if self.draft: path += 'draft/' path += title.replace('^', '^') \ .replace('$', '$') \ .replace('*', '*') \ .replace('%', '%') \ .replace('?', '?') \ .replace('/', '/') return path class GrowiPage: """ GROWI ページを表す Attributes ---------- id : str ページ ID path : str パス revision : str リビジョン title : str タイトル tags : array タグ content : str 本文 """ def __init__(self, id, path, revision, title, tags, content): """ Parameters ---------- id : str ページ ID path : str パス revision : str リビジョン title : str タイトル tags : array タグ content : str 本文 """ self.id = id self.path = path self.revision = revision self.title = title self.tags = tags self.content = content self.attachments = {} def replace_attachment(self, file_name, file_path_proxied): """ 本文の指定されたファイルの参照 (リンク) を指定された参照 (リンク) に置き換える Parameters ---------- file_name : str 置き換えるフアイル名 file_path_proxied : str 置き換える参照 (リンク) """ self.content = re.sub(r'(!\[' + file_name + r'\])\(.+\)', r'\1(' + file_path_proxied + r')', self.content) def initialize_attachments_info(self, attachments_list_res): """ GROWI ページの添付ファイル情報を設定する Parameters ---------- attachments_list_res : json _api/v3/attachment/list の返却データ """ for attachment in attachments_list_res['paginateResult']['docs']: self.attachments[attachment['id']] \ = GrowiAttachment(attachment['id'], attachment['originalName'], attachment['filePathProxied']) def add_attachment_info(self, attachment_id, original_name, file_path_proxied): """ 添付ファイル情報を追加する Parameters ---------- id : str 添付ファイル ID original_name : str オリジナルのファイル名 file_path_proxied : str 添付ファイルの参照パス (リンク) """ self.attachments[attachment_id] = GrowiAttachment(attachment_id, original_name, file_path_proxied) def get_attachment_info(self, file_name): """ この GROWI ページが指定されたファイル名の添付ファイル情報を返す Parameters ---------- file_name : str ファイル名 Retruns ------- attachment : GrowiAttachment 添付ファイル情報 添付ファイルが存在しない場合は None """ attachment = [self.attachments[id] for id in self.attachments if self.attachments[id].original_name == file_name] if attachment: return attachment[0] return None def remove_attachment_info(self, attachment_id): """ この GROWI ページから指定された添付ファイル ID の添付ファイル情報を削除する Parameters ---------- attachment_id : str 添付ファイル ID """ del self.attachments[attachment_id] class GrowiAttachment: """ GROWI 添付ファイルを表す Attributes ---------- id : str 添付ファイル ID original_name : str オリジナルのファイル名 file_path_proxied : str 添付ファイルの参照パス (リンク) """ def __init__(self, id, original_name, file_path_proxied): """ Parameters ---------- id : str 添付ファイル ID original_name : str オリジナルのファイル名 file_path_proxied : str 添付ファイルの参照パス (リンク) """ self.id = id self.original_name = original_name self.file_path_proxied = file_path_proxied
使い方は
- GrowiClient のインスタンスを作る
- create_page で新しい記事を作成する
- 添付ファイルがあれば set_attachment で追加する
- 必要なら記事内の添付ファイルの参照 url を書き換える
- 添付ファイルの数分実施する
- 添付ファイルの url を書き換えたり、タグがあるなら update_page で記事を更新する
- 記事の数分、2〜4 を繰り返す
次のコードは、新しく記事を作って、ファイルを添付して、さらにそれを更新するものです。
from growiclient import GrowiClient growi_client = GrowiClient('growi', '3000', 'rMVCHsrPDuN7wlZfVOn9lWgqC5flSd2yjtqffO4T4aw=', 'tiger') title = 'GlowiClientで作成したページ' content = """\ このページは Python で作成した GLOWI Client で作成しました。 ![tora.png](/tora.png) """ page = growi_client.create_page(title, ['GlowiClient', 'TEST'], content) file_url = growi_client.set_attachment(page, './knowledge_to_growi/attachments/tora.png') page.replace_attachment('tora.png', file_url) growi_client.update_page(page) content = page.content + \ """ \ \n ページを更新します。 ![ojiisan.png](/ojiisan.png) """ page = growi_client.create_page(page.title, page.tags, content) file_url = growi_client.set_attachment(page, './knowledge_to_growi/attachments/ojiisan.png') page.replace_attachment('ojiisan.png', file_url) growi_client.update_page(page)
こんな、記事が作成されます。
Knowledge→GROWI 移行 (5)
GROWI API の使い方を調べる
GROWI API の使い方を調べます。ドキュメントは下記のページにあります。
私にはドキュメント読んでも具体的なイメージがわかなかったので、やはり Google で検索すると、私にピッタリなサイトが見つかりました。
まずは、そのまま使わせていただきます。requests モジュールが必要なので、.devcontainer/python/requirement.txt に requests を追加してコンテナを再ビルドしました。
そういえば、以前 PostgreSQL にアクセスするのに psycopg2 モジュールを使いました。この時も .devcontainer/python/requirement.txt に psycopg2 を追加してコンテナを再ビルドしました。
簡単に動かしてみます
from crclient import CrClient crclient = CrClient('growi', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'tiger', './knowledge_to_growi/contents') crclient.fetch()
確認用に記事を 1 件だけ登録してあるのですが、なんか取得できているようです。
# cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 43313 -- /workspace/knowledge_to_growi/crclient_sample.py skip item: {'_id': '61f9120a32a52003888290b5', 'status': 'published', 'grant': 1, 'grantedUsers': [], 'liker': [], 'seenUsers': ['61f7bcc6834bc4803f0e2bfa'], 'commentCount': 0, 'createdAt': '2022-02-01T10:57:14.468Z', 'updatedAt': '2022-02-01T10:57:14.488Z', 'path': '/user/tiger/自分自身のこと', 'creator': '61f7bcc6834bc4803f0e2bfa', 'lastUpdateUser': {'_id': '61f7bcc6834bc4803f0e2bfa', 'isGravatarEnabled': False, 'isEmailPublished': True, 'lang': 'ja_JP', 'status': 2, 'admin': False, 'createdAt': '2022-01-31T10:41:10.624Z', 'username': 'tiger', 'email': 'tiger@example.com', 'lastLoginAt': '2022-02-01T10:49:07.549Z', 'imageUrlCached': '/images/icons/user.svg', 'name': 'tiger62shin'}, 'redirectTo': None, 'grantedGroup': None, '__v': 1, 'revision': '61f9120a32a52003888290b9'} skip item: {'_id': '61f7bd73834bc4803f0e2c25', 'status': 'published', 'grant': 1, 'grantedUsers': ['61f7bcc6834bc4803f0e2bfa'], 'liker': [], 'seenUsers': ['61f7bcc6834bc4803f0e2bfa'], 'commentCount': 0, 'createdAt': '2022-01-31T10:44:03.214Z', 'updatedAt': '2022-01-31T10:44:03.239Z', 'path': '/user/tiger', 'creator': '61f7bcc6834bc4803f0e2bfa', 'lastUpdateUser': {'_id': '61f7bcc6834bc4803f0e2bfa', 'isGravatarEnabled': False, 'isEmailPublished': True, 'lang': 'ja_JP', 'status': 2, 'admin': False, 'createdAt': '2022-01-31T10:41:10.624Z', 'username': 'tiger', 'email': 'tiger@example.com', 'lastLoginAt': '2022-02-01T10:49:07.549Z', 'imageUrlCached': '/images/icons/user.svg', 'name': 'tiger62shin'}, 'redirectTo': None, 'grantedGroup': None, '__v': 1, 'revision': '61f7bd73834bc4803f0e2c2b'}```
このソースを読んで、動きを確認したことで、なんとなく感覚はつかめたような気がします。API である URL にパラメタを付けて GET / POST でリクエストすると結果が JSON で帰ってくるという一般的な Web API と理解しました。
当初、この CrClient をそのままか、若干の修正で使わせていただこうと思っていたのですが、
- データ移行なので更新の機能は必要ない
- 1 回だけ新規登録できれば良い。失敗したらマニュアルで削除して再実行すれば良い
- 添付ファイルが付けられない
ということで、やはり自分で作ることにしました。
必要な機能は多くはなく
- 記事の新規登録
- 記事へのファイル添付
添付したファイルの url が変わるはずなので、下記のような手順になると思う- 記事を新規登録する
- 記事にファイルを添付する
- 記事の添付ファイル参照 URL を書き換える
以下では機能ごとに調べていきますが、先にこれまで記載したサイトの他に参考にさせて頂いたのですがサイトを載せておきます。
あと、GROWI API のドキュメントのサイトにある OpenAPI specification はよく参照しました。
以下の GROWI の API 利用の例では、今回のデータ移行に必要なパラメータやレスポンスのみ記載しました。
記事の新規登録
項目 | 値 |
---|---|
URL | _api/v3/pages |
Method | POST |
Query Paramaters | access_token : API Token user : ユーザID |
Payload | body : 本文 path : 記事のパス |
Response | id : 記事のID path : 記事のパス revision : 記事のリビジョン |
import requests url = 'http://growi:3000/_api/v3/pages' params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "user": "tiger"} path = "新規記事登録のサンプル" content = "この記事は GROWI API を使って登録した記事です。" payload = {"body": content, "path": "/tiger/{}".format(path)} res = requests.post(url, data=payload, params=params) pages_res = res.json() print('id : ' + pages_res['page']['id']) print('path : ' + pages_res['page']['path']) print('revision : ' + pages_res['page']['revision'])
このプログラムを実行するとコンソールに下記のように出力されます。
root@8949de830c08:/workspace# /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 39727 -- /workspace/knowledge_to_growi/growiapi_sample1.py id : 61fa6161a03e4c6720a5f36a path : /tiger/新規記事登録のサンプル revision : 61fa6161a03e4c6720a5f36e
ブラウザで確認してみます。
ちゃんと登録されました。
記事へのファイル添付
項目 | 値 |
---|---|
URL | _api/attachments.add |
Method | POST |
Query Paramaters | access_token : API Token user : ユーザID |
Payload | page_id : 記事のID path : 記事のバス 記事の新規登録で戻ってきた値を指定 |
file | ファイル名, ファイル本体 |
Response | id : 添付ファイルのID originalName : ファイル名 filePathProxied : 添付ファイルのパス |
先に作成した記事に下記の画像を添付してみます。
import requests url = 'http://growi:3000/_api/attachments.add' params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "user": "tiger"} file = {'file': ('setsubun_mamemaki.png', open('./knowledge_to_growi/attachments/setsubun_mamemaki.png', 'rb'), 'image/png')} payload = {'page_id': '61fa6161a03e4c6720a5f36a', 'path': '/tiger/新規記事登録のサンプル'} res = requests.post(url, data=payload, files=file, params=params) res.raise_for_status attachments_res = res.json() print('id : {} , originalName : {} , filePathProxied : {} ' .format(attachments_res['attachment']['id'], attachments_res['attachment']['originalName'], attachments_res['attachment']['filePathProxied']))
このプログラムを実行するとコンソールに下記のように出力されます。
cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2022.0.1786462952/pythonFiles/lib/python/debugpy/launcher 40267 -- /workspace/knowledge_to_growi/growiapi_sample2.py id : 61fe69c0f6ed08f016d32137 , originalName : setsubun_mamemaki.png , filePathProxied : /attachment/61fe69c0f6ed08f016d32137 root@83fe8d69003a:/workspace#
ブラウザで確認してみます。
ちゃんと添付されました。
記事の更新
項目 | 値 |
---|---|
URL | _api/pages.update |
Method | POST |
Query Paramaters | access_token : API Token user : ユーザID |
Payload | body : 本文 pageTags : タグのリスト page_id : 記事のID (*) path : 記事のバス (*) revision_id : リビジョンID(*) (*) 記事の新規登録で戻ってきた値を指定 |
Response | revision : 記事のリビジョン (*) (*)更新するたびにリビジョンが変わるので何度も更新する場合は注意が必要 |
データ移行では本来、記事を更新する必要はないのですが、記事内の添付ファイル参照の url を書き換える必要があります。
下のソースは最初に登録した記事に添付ファイルの画像を表示するように更新するものです。
import requests url = 'http://growi:3000/_api/pages.update' params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "user": "tiger"} content = 'この記事は GROWI API を使って登録した記事です。' \ '\n\n' \ '![setsubun_mamemaki.png](/attachment/61fe69c0f6ed08f016d32137)' payload = {'body': content, 'page_id': '61fa6161a03e4c6720a5f36a', 'path': '/tiger/新規記事登録のサンプル', 'revision_id': '61fa6161a03e4c6720a5f36e'} res = requests.post(url, data=payload, params=params) res.raise_for_status
ブラウザで確認してみます。
記事が更新され、画像が表示されました。
以上で、データ移行に必要な GROWI API の使い方はわかったのですが、その他にも以下のような API について使い方を調べました。
記事の一覧
項目 | 値 |
---|---|
URL | _api/pages.list |
Method | GET |
Query Paramaters | access_token : API Token user : ユーザID limit : 最大取得件数、-1 を指定すると全件 |
Payload | N/A |
Response | _id : 記事のID path : 記事のパス revision : 記事のリビジョン |
今回のデータ移行では、最初に全ての記事のリストを取得しておき、移行対象記事の存在チェックに利用しました。
import requests url = 'http://growi:3000/_api/pages.list' params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 'user': 'tiger', 'limit': -1} res = requests.get(url, params=params) res.raise_for_status pages_list_res = res.json() for page in pages_list_res['pages']: print('id : {} / path : {} / revision : {} ' .format(page['_id'], page['path'], page['revision']))
このプログラムを実行するとコンソールに下記のように出力されます。
# /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2022.0.1786462952/pythonFiles/lib/python/debugpy/launcher 41573 -- /workspace/knowledge_to_growi/growiapi_sample4.py id : 61fa6161a03e4c6720a5f36a / path : /tiger/新規記事登録のサンプル / revision : 61fa7643a03e4c6720a5f4f2 id : 61f9120a32a52003888290b5 / path : /user/tiger/自分自身のこと / revision : 61f9120a32a52003888290b9 id : 61f7bd73834bc4803f0e2c25 / path : /user/tiger / revision : 61f7bd73834bc4803f0e2c2b
添付ファイルの一覧
項目 | 値 |
---|---|
URL | _api/v3/attachment/list |
Method | GET |
Query Paramaters | access_token : API Token user : ユーザID pageid : 記事の ID (*) page : 添付ファイル一覧のページ番号 (*) 記事の新規登録 / 記事の一覧の取得で戻ってきた値を指定 |
Payload | N/A |
Response | id : 添付ファイルのID originalName : 添付ファイル名 filePathProxied : 添付ファイルの url パス |
この API は少し注意が必要です。取得できる添付ファイル情報は画面に表示される添付ファイル一覧の 1 ページ分しか取得できません。幸い page パラメーターで取得する一覧のページ番号を指定できますので、データが取得できなくなるまでページ番号をインクリメントしながら取得します。
今回のデータ移行では、すでに存在している記事に対して移行する際の添付ファイルの存在チェックに利用しました。
import requests url = 'http://growi:3000/_api/v3/attachment/list' params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 'user': 'tiger', 'pageId': '61fa6161a03e4c6720a5f36a', 'page': 1} res = requests.get(url, params=params) res.raise_for_status attachment_list_res = res.json() for attachment in attachment_list_res['paginateResult']['docs']: print('id : {} , originalName : {} , filePathProxied : {} ' .format(attachment['id'], attachment['originalName'], attachment['filePathProxied']))
このプログラムを実行するとコンソールに下記のように出力されます。
# cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2022.0.1786462952/pythonFiles/lib/python/debugpy/launcher 41445 -- /workspace/knowledge_to_growi/growiapi_sample5.py id : 61fa6799a03e4c6720a5f490 , originalName : setsubun_mamemaki.png , filePathProxied : /attachment/61fa6799a03e4c6720a5f490
添付ファイルの削除
項目 | 値 |
---|---|
URL | _api/attachments.remove |
Method | POST |
Query Paramaters | access_token : API Token user : ユーザID |
Payload | attachment_id : 添付ファイルID 記事へのファイル添付 / 添付ファイルの一覧取得で戻ってきた値を指定 |
Response | - |
今回のデータ移行では、すでに存在している添付ファイルを置き換える際の削除に利用しました。
import requests url = 'http://growi:3000/_api/attachments.remove' params = {"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 'user': 'tiger'} payload = {'attachment_id': '61fa6799a03e4c6720a5f490'} res = requests.post(url, data=payload, params=params) res.raise_for_status
このサンプルでは「記事へのファイル添付」で添付したファイルを削除しています。
添付ファイルが削除されたので表示されなくなりました。
以上です。
あと、既存の記事の取得とかあればよかったのでしょうが、今回のデータ移行には必要なかったので調べませんでした。
Knowledge→GROWI 移行 (4)
Visual Studio Code に GROWI の Docker コンテナを追加
GROWI は元々が Docker コンテナで導入するのが基本のようなので、それを利用します。
テスト用だし git clone する必要はないので、ここ から growi-docker-compose を zip ファイルでダウンロードします。
ダウンロードしたファイルを作業ディレクトリに解凍します。
% unzip growi-docker-compose-master.zip % ls -l growi-docker-compose-master total 64 -rw-r--r--@ 1 tiger staff 412 12 30 23:02 Dockerfile -rw-r--r--@ 1 tiger staff 980 12 30 23:02 Dockerfile.v42x -rw-r--r--@ 1 tiger staff 1069 12 30 23:02 LICENSE -rw-r--r--@ 1 tiger staff 3612 12 30 23:02 README.md -rw-r--r--@ 1 tiger staff 1266 12 30 23:02 docker-compose.dev.yml -rw-r--r--@ 1 tiger staff 2217 12 30 23:02 docker-compose.v42x.yml -rw-r--r--@ 1 tiger staff 2305 12 30 23:02 docker-compose.v43x-v446.yml -rw-r--r--@ 1 tiger staff 2429 12 30 23:02 docker-compose.yml drwxr-xr-x@ 4 tiger staff 128 12 30 23:02 elasticsearch drwxr-xr-x@ 7 tiger staff 224 12 30 23:02 examples drwxr-xr-x@ 5 tiger staff 160 12 30 23:02 hackmd
かるく(?) docker-compose.yml を覗いてみます。growi 本体と mongodb, elasticsearch の 3 つのコンテナで構成されているようです。
.devcontainer フォルダの直下に GROWI 用の Dockerfile を置くフォルダ growi を作成して growi-docker-compose-master/Dockerfile, LICENSE, README.md をコビーします (LICENSE, README.md は動作には必要ないですが)。
以下の例はカレントディレクトがプロジェクトフォルダでプロジェクトフォルダ直下に growi-docker-compose-master.zip を展開しています。
% mkdir -p .devcontainer/growi % cp growi-docker-compose-master/Dockerfile .devcontainer/growi % cp growi-docker-compose-master/LICENSE .devcontainer/growi % cp growi-docker-compose-master/README.md .devcontainer/growi
次に growi-docker-compose-master/elasticsearch をフォルダごと .devcontainer にコピーします。
% cp -R growi-docker-compose-master/elasticsearch .devcontainer
.devcontainer/docker-compose.yml に GROWI の設定を記述するのですが、基本的には growi-docker-compose-master/docker-compose.yml の内容を追記すれば良いはずです。
version: '3' services: python38: restart: always build: python container_name: 'python38' working_dir: '/workspace' tty: true extra_hosts: - "fluorine.kt.asasystems.co.jp:192.168.100.78" environment: - DISPLAY=${IP_ADDR}:0.0 volumes: - ..:/workspace:cached - /tmp/.X11-unix:/tmp/.X11-unix postgres: restart: always build: postgres container_name: 'postgres12' ports: - 5433:5433 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - ./postgres/initdb:/docker-entrypoint-initdb.d growi: build: growi container_name: 'growi' ports: - 127.0.0.1:3000:3000 # localhost only by default links: - mongo:mongo - elasticsearch:elasticsearch depends_on: - mongo - elasticsearch environment: - MONGO_URI=mongodb://mongo:27017/growi - ELASTICSEARCH_URI=http://elasticsearch:9200/growi - PASSWORD_SEED=changeme - FILE_UPLOAD=mongodb # activate this line if you use MongoDB GridFS rather than AWS # - FILE_UPLOAD=local # activate this line if you use local storage of server rather than AWS # - MATHJAX=1 # activate this line if you want to use MathJax # - PLANTUML_URI=http:// # activate this line and specify if you use your own PlantUML server rather than public plantuml.com # - HACKMD_URI=http:// # activate this line and specify HackMD server URI which can be accessed from GROWI client browsers # - HACKMD_URI_FOR_SERVER=http://hackmd:3000 # activate this line and specify HackMD server URI which can be accessed from this server container # - FORCE_WIKI_MODE='public' # activate this line to force wiki public mode # - FORCE_WIKI_MODE='private' # activate this line to force wiki private mode entrypoint: "dockerize -wait tcp://mongo:27017 -wait tcp://elasticsearch:9200 -timeout 60s /docker-entrypoint.sh" command: ["yarn migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"] restart: unless-stopped volumes: - growi_data:/data mongo: image: mongo:4.4 container_name: 'mongo' restart: unless-stopped volumes: - mongo_configdb:/data/configdb - mongo_db:/data/db elasticsearch: build: elasticsearch container_name: 'elasticsearch' environment: - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms256m -Xmx256m" # increase amount if you have enough memory - LOG4J_FORMAT_MSG_NO_LOOKUPS=true # CVE-2021-44228 mitigation for Elasticsearch <= 6.8.20/7.16.0 ulimits: memlock: soft: -1 hard: -1 restart: unless-stopped volumes: - es_data:/usr/share/elasticsearch/data - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml volumes: growi_data: driver_opts: type: none device: /Users/user/workspace/project/ToolsByPython/docker_volumes/growi_data o: bind mongo_configdb: driver_opts: type: none device: /Users/user/workspace/project/ToolsByPython/docker_volumes/mongo_configdb o: bind mongo_db: driver_opts: type: none device: /Users/user/workspace/project/ToolsByPython/docker_volumes/mongo_db o: bind es_data: driver_opts: type: none device: /Users/user/workspace/project/ToolsByPython/docker_volumes/es_data o: bind es_plugins: driver_opts: type: none device: /Users/user/workspace/project/ToolsByPython/docker_volumes/es_plugins o: bind
変更点は以下の通りです。
- growi コンテナ
- mongo コンテナと elasticsearch コンテナは変更なし
- volumes
デフォルトのままでもよかったが、なんとなくローカルのディレクトリを指定した。
最初、.devcontainer ディレクトリ下のディレクトリを指定したところ、パスに隠しディレトリがあるとコンテナのビルドでエラーになってしまいました。パスに隠しディレクトリを含まない場所にディレクトリを作成して指定しました。
設定は以上で終了です。Visual Studio Code を起動します。Docker コンテナが正常に起動されたならブラウザから http://localhost:3000 にアクセスして GROWI が正しく起動されていることを確認します。
上の画面から管理者ユーザを登録して GROWI の初期設定を行います。データ移行のテスト用なので、最低限の設定しか行いませんでした。
上の図のように「サイトURL」のみ設定しました。
次に実際に記事の移行先となる一般ユーザを登録して、プログラムから GROWI API を利用するための API Token を取得します。
- 管理者により「ユーザー管理」から一般ユーザの仮パスワードを発行
- 一般ユーザのメールアドレスと仮パスワードでログイン
- ユーザ情報の入力
API Token を発行
ページ右上に表示されているユーザ ID をクリックして [設定] ボタンをクリックします。
これで、Knowledge→GROWI データ移行のためのテスト環境が整いました。長かった。
毎度のことですが、プログラミングって環境作るのが大変で、経験したことない人は環境作るところで挫折するっていう話をどこかで聞いた (読んだ) ことがありますが、本当な気がします。
Knowledge→GROWI 移行 (3)
Python で Knowledge のデータベース (PostgreSQL) にアクセスしてみる
Python で PostgreSQL にアクセスするする方法はさっぱり分かりませんから、いつものように Google で検索します。
たくさんヒットしますが、下記のサイトを参考にさせていただきました。
まずは、knowledges テーブルから knowledge_id と title を取り出してみます。全部取り出すと大変なので、条件をつけて 5 件だけ取り出します。
import psycopg2 from psycopg2.extras import DictCursor # DB接続情報 DB_HOST = 'postgres' DB_PORT = '5432' DB_NAME = 'knowledgedb' DB_USER = 'kbadmin' DB_PASS = '**********' # DB接続関数 def get_connection(): return psycopg2.connect('postgresql://{user}:{password}@{host}:{port}/{dbname}' .format(user=DB_USER, password=DB_PASS, host=DB_HOST, port=DB_PORT, dbname=DB_NAME)) with get_connection() as conn: with conn.cursor(cursor_factory=DictCursor) as cur: cur.execute('select knowledge_id, title from knowledges ' 'where knowledge_id in (217, 227, 229, 236, 240)') for row in cur: print(str(row['knowledge_id']) + ' : ' + row['title'])
参考にさせてもらったサイトのサンプルを幾つか組み合わせただけです。取得した結果から列のデータを辞書式で取得したかったので、DictCursor を使いました。
結果はこんな感じです。
root@ced5581a8a4a:/workspace# cd /workspace ; /usr/bin/env /usr/local/bin/python3.8 /root/.vscode-server/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 41367 -- /workspace/knowledge_to_growi/postgres_sample.py 240 : Mac 関連のメモ 229 : Visual Studio Code 設定 227 : Docker 236 : POP3 サーバ内メールを取得して IMAP メールボックスに配信 217 : ORACLE メモ
続けて、添付ファイルを取得してみます。
cur.execute('select file_no, knowledge_id, comment_no, draft_id, file_name, file_binary ' 'from knowledge_files where file_no = 556') for row in cur: print(row['file_name']) file_name = './knowledge_to_growi/attachments/' \ + row['file_name'] with open(file_name, 'wb') as f: f.write(row['file_binary'])
添付ファイルもちゃんと取得できました。ところが、このままではちょっと問題があります。
私が実現したいのは「見出しや本文を取り出しつつ、その添付ファイルも取り出す」ことなので、上のコーディングだと cursor が 1 つしかないので knowledges から「見出し」や「本文」を一旦全部取り出してから、knowledge_files から添付ファイルを取り出すようにしないと上手くいかないと思います。それでもいいのですが、あまり好きな方法ではないので knowledges 用と knowledge_files 用の 2 つのカーソルを使います。ついでに目的の記事に添付されているファイルのみを取り出すようにします。
with conn.cursor(cursor_factory=DictCursor) as knowledges_cur: knowledges_cur.execute('select knowledge_id, title from knowledges ' 'where knowledge_id in (217, 227, 229, 236, 240)') for knowledge_row in knowledges_cur: print(str(knowledge_row['knowledge_id']) + ' : ' + knowledge_row['title']) with conn.cursor(cursor_factory=DictCursor) as file_cur: file_cur.execute('select file_no, knowledge_id, comment_no, draft_id, file_name, file_binary ' 'from knowledge_files ' 'where knowledge_id = %s', (knowledge_row['knowledge_id'], )) for file_row in file_cur: print(file_row['file_name']) file_name = './knowledge_to_growi/attachments/' + file_row['file_name'] with open(file_name, 'wb') as f: f.write(file_row['file_binary'])
だいぶ、ネストが深くなりましたが、これで knowledges テーブルから記事を取り出しつつ、knowledge_files テーブルからその記事の添付ファイルを取り出すことが出来ました。
draft_knowledges からも同様の方法で取得できるはずです。
これで、GROWI に移行するデータを取得する目処がたちました。
Knowledge→GROWI 移行 (2)
Visual Studio Code に PostgreSQL の Docker コンテナを追加
コーディングまでの道のりは長い。
上の記事で Visual Studio Code で Python の実行環境である Docker コンテナに接続して Python のプログラムが実行できるところまでは設定しましたが、Knowledge から移行データを抜くためのデータベース環境を作っていませんでした。
今回は Visual Studio Code に PostgreSQL の Docker コンテナを追加します。
まずは、Google で PostgreSQL の Dockerfile の雛形を探します。ありがたいことに手順が解説されているサイトがたくさん見つかります。私は下記のサイトを参考にさせていただきました。
まず、.devcontainer フォルダの直下に PostgreSQL 用の Dockerfile 等を置くフォルダを作成します。
.devcontainer/postgres
このフォルダに上のサイトを参考にして Dockerfile を作成します。
FROM postgres:12-alpine ENV LANG ja_JP.UTF-8 ENV LANGUAGE ja_JP:ja ENV LC_ALL ja_JP.UTF-8 ENV TZ JST-9
ENV は LANG だけあれば良いようですが、他のコンテナでも設定しているので、LANGUAGE, LC_ALL, TZ も設定しておきました。特に意味はないです。
このままでも PostgreSQL のコンテナは問題なく起動しますが、目的のデータベースも何もない状態なのでコンテナのビルド時に移行対象のデータベースをインポートしたい思います。ここでも Google 先生に頼ります。下記のようなサイトが見つかりました。
/docker-entrypoint-initdb.d に置かれた .sql / .sh ファイルが 1 度だけ実行されるそうです。では、/docker-entrypoint-initdb.d に置くファイルを準備します。
まず、移行元サーバーから移行元データベースのダンプを取得してきます。
$ pg_dump knowledgedb -U kbadmin >knowledgedb.dump
次にデータベース / ユーザを作成する SQL ファイルを準備します。ファイル名は createdb.sql とします。
CREATE ROLE kbadmin LOGIN PASSWORD '**********'; ALTER ROLE kbadmin SUPERUSER; create database knowledgedb;
最後にデータベース / ユーザを作成してデータをインポートするシェル (01-initdb.sh) を作成します。
#!/bin/sh PGPASSWORD=********** psql < /docker-entrypoint-initdb.d/sql/createdb.sql psql -U kbadmin -d knowledgedb < /docker-entrypoint-initdb.d/sql/knowledgedb.dump
これらのファイルを下図のように .devcontainer/postgres フォルダに配置します。
docker-compose.yml に PostgreSQL の設定を追加します。
version: '3' services: python38: restart: always build: python container_name: 'python38' working_dir: '/workspace' tty: true extra_hosts: - "fluorine.kt.asasystems.co.jp:192.168.100.78" environment: - DISPLAY=${IP_ADDR}:0.0 volumes: - ..:/workspace:cached - /tmp/.X11-unix:/tmp/.X11-unix postgres: restart: always build: postgres container_name: 'postgres12' ports: - 5433:5433 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: ********** volumes: - ./postgres/initdb:/docker-entrypoint-initdb.d
postgres の公式 docker image は envrionement の値を読みとり初期設定を行なう機能があるそうです。私は POSTGRES_USER と POSTGRES_PASSWORD を指定しました。POSTGRES_DB を省略すると POSTGRES_USER で指定した名前と同じ名前のデータベースが作成されるそうです。
あと、volumes でデータベースを初期化するシェルのあるフォルダを /docker-entrypoint-initdb.d に割り当てます。
これで、Docker コンテナの設定は完了しました。
Visual Studio Code を起動してみます。エラーなく起動できたら Python と PostgreSQL の Docker コンテナが起動しているか確認してみます。
% docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 02ee35af85df toolsbypython_devcontainer_postgres "docker-entrypoint.s…" 22 hours ago Up 6 minutes 5432/tcp, 0.0.0.0:5433->5433/tcp postgres12 ced5581a8a4a toolsbypython_devcontainer_python38 "python3" 22 hours ago Up 6 minutes 6006/tcp python38
問題なさそうです。では、Knowledge のデータベースが作成されているかどうかを確認してみます。
まず、PostgreSQL の Docker コンテナにログインします。私は Docker コンテナにログインするのに下記のようなシェルを作成しています。
docker-shell.sh
#!/bin/sh docker exec -i -t `docker ps | grep "$1" | awk '{print $1;}'` /bin/bash
では、ログイン
% docker-shell.sh postgres bash-5.1#
データベースが作成 (インポート) されているか確認するために psql を起動してテーブルの一覧でも表示してみます。
bash-5.1# psql -U kbadmin -d knowledgedb psql (12.9) Type "help" for help. knowledgedb=# \d List of relations Schema | Name | Type | Owner --------+-------------------------------+----------+--------- public | access_logs | table | kbadmin public | access_logs_no_seq | sequence | kbadmin public | account_images | table | kbadmin public | account_images_image_id_seq | sequence | kbadmin public | activities | table | kbadmin public | activities_activity_no_seq | sequence | kbadmin public | badges | table | kbadmin public | badges_no_seq | sequence | kbadmin public | comments | table | kbadmin public | comments_comment_no_seq | sequence | kbadmin public | confirm_mail_changes | table | kbadmin public | draft_item_values | table | kbadmin public | draft_knowledges | table | kbadmin public | draft_knowledges_draft_id_seq | sequence | kbadmin public | events | table | kbadmin public | functions | table | kbadmin public | groups | table | kbadmin public | groups_group_id_seq | sequence | kbadmin public | hash_configs | table | kbadmin public | item_choices | table | kbadmin public | knowledge_edit_groups | table | kbadmin public | knowledge_edit_users | table | kbadmin public | knowledge_files | table | kbadmin public | knowledge_files_file_no_seq | sequence | kbadmin public | knowledge_groups | table | kbadmin public | knowledge_histories | table | kbadmin public | knowledge_item_values | table | kbadmin public | knowledge_tags | table | kbadmin public | knowledge_users | table | kbadmin public | knowledges | table | kbadmin public | knowledges_knowledge_id_seq | sequence | kbadmin public | ldap_configs | table | kbadmin public | like_comments | table | kbadmin public | like_comments_no_seq | sequence | kbadmin public | likes | table | kbadmin public | likes_no_seq | sequence | kbadmin public | locales | table | kbadmin public | login_histories | table | kbadmin public | mail_configs | table | kbadmin public | mail_hook_conditions | table | kbadmin public | mail_hook_ignore_conditions | table | kbadmin public | mail_hooks | table | kbadmin public | mail_hooks_hook_id_seq | sequence | kbadmin public | mail_locale_templates | table | kbadmin --More--
なんか大丈夫そうです。2, 3のテーブルについて select してみましたが中身が入っているので問題ないでしょう。