ほぼ老人のプログラミング日記

定年後の平凡なサラリーマンの趣味の日記

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 つが読み込めませんでした。間違ったフォーマットのプログラムを読み込もうとしました。

といった、エラーが発生してテストが実行できなかったが、意外なところに設定があった。

f:id:tiger62shin:20220214170947p:plain

「既定のプロセッサ アーキテクチャ」を「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

使い方は

  1. GrowiClient のインスタンスを作る
    • draft を true にすると、ユーザディレクトリの下に "draft" という名前のディレクトリを作ってそこに記事を作成します。
  2. create_page で新しい記事を作成する
  3. 添付ファイルがあれば set_attachment で追加する
    • 必要なら記事内の添付ファイルの参照 url を書き換える
    • 添付ファイルの数分実施する
  4. 添付ファイルの url を書き換えたり、タグがあるなら update_page で記事を更新する
  5. 記事の数分、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)

こんな、記事が作成されます。 f:id:tiger62shin:20220207140051p:plain

Knowledge→GROWI 移行 (5)

GROWI API の使い方を調べる

GROWI API の使い方を調べます。ドキュメントは下記のページにあります。

docs.growi.org

私にはドキュメント読んでも具体的なイメージがわかなかったので、やはり Google で検索すると、私にピッタリなサイトが見つかりました。

kapibara-sos.net

まずは、そのまま使わせていただきます。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 が変わるはずなので、下記のような手順になると思う
    1. 記事を新規登録する
    2. 記事にファイルを添付する
    3. 記事の添付ファイル参照 URL を書き換える

以下では機能ごとに調べていきますが、先にこれまで記載したサイトの他に参考にさせて頂いたのですがサイトを載せておきます。

s-densan.hatenablog.com

あと、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

ブラウザで確認してみます。

f:id:tiger62shin:20220202195004p:plain

ちゃんと登録されました。

記事へのファイル添付

項目
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 : 添付ファイルのパス

先に作成した記事に下記の画像を添付してみます。

f:id:tiger62shin:20220202195818p:plain

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# 

ブラウザで確認してみます。

f:id:tiger62shin:20220202201534p:plain

ちゃんと添付されました。

記事の更新

項目
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

ブラウザで確認してみます。

f:id:tiger62shin:20220202211747p:plain

記事が更新され、画像が表示されました。


以上で、データ移行に必要な 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

このサンプルでは「記事へのファイル添付」で添付したファイルを削除しています。

f:id:tiger62shin:20220204212349p:plain

添付ファイルが削除されたので表示されなくなりました。


以上です。
あと、既存の記事の取得とかあればよかったのでしょうが、今回のデータ移行には必要なかったので調べませんでした。

Knowledge→GROWI 移行 (4)

Visual Studio Code に GROWI の Docker コンテナを追加

GROWI は元々が Docker コンテナで導入するのが基本のようなので、それを利用します。

docs.growi.org

テスト用だし 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 コンテナ
    • サービス名が app になっているので、growi に変更
    • カレントディレクトリの Dockerfile をビルドする様になっているで growi ディレクトリ下の Dockerfile をビルドするように変更
    • コンテナ名を "growi" に設定
    • FILE_UPLOAD に "mongodb" を指定して、添付ファイルを MongoDB に格納するよう設定
  • mongo コンテナと elasticsearch コンテナは変更なし
  • volumes
    デフォルトのままでもよかったが、なんとなくローカルのディレクトリを指定した。
    最初、.devcontainer ディレクトリ下のディレクトリを指定したところ、パスに隠しディレトリがあるとコンテナのビルドでエラーになってしまいました。パスに隠しディレクトリを含まない場所にディレクトリを作成して指定しました。


設定は以上で終了です。Visual Studio Code を起動します。Docker コンテナが正常に起動されたならブラウザから http://localhost:3000 にアクセスして GROWI が正しく起動されていることを確認します。

f:id:tiger62shin:20220131193511p:plain

上の画面から管理者ユーザを登録して GROWI の初期設定を行います。データ移行のテスト用なので、最低限の設定しか行いませんでした。

f:id:tiger62shin:20220131193954p:plain

上の図のように「サイトURL」のみ設定しました。
次に実際に記事の移行先となる一般ユーザを登録して、プログラムから GROWI API を利用するための API Token を取得します。

  • 管理者により「ユーザー管理」から一般ユーザの仮パスワードを発行
  • 一般ユーザのメールアドレスと仮パスワードでログイン
  • ユーザ情報の入力
  • API Token を発行
    ページ右上に表示されているユーザ ID をクリックして [設定] ボタンをクリックします。

    f:id:tiger62shin:20220131194634p:plain

    API設定」タブの [API Token を更新] ボタンをクリックして API Token を発行します。


これで、Knowledge→GROWI データ移行のためのテスト環境が整いました。長かった。
毎度のことですが、プログラミングって環境作るのが大変で、経験したことない人は環境作るところで挫折するっていう話をどこかで聞いた (読んだ) ことがありますが、本当な気がします。

Knowledge→GROWI 移行 (3)

Python で Knowledge のデータベース (PostgreSQL) にアクセスしてみる

PythonPostgreSQL にアクセスするする方法はさっぱり分かりませんから、いつものように Google で検索します。

たくさんヒットしますが、下記のサイトを参考にさせていただきました。

chusotsu-program.com

qiita.com

まずは、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'])

f:id:tiger62shin:20220131151018p:plain 添付ファイルもちゃんと取得できました。ところが、このままではちょっと問題があります。
私が実現したいのは「見出しや本文を取り出しつつ、その添付ファイルも取り出す」ことなので、上のコーディングだと 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 CodePostgreSQL の Docker コンテナを追加

コーディングまでの道のりは長い。

tiger62shin.hatenablog.com

上の記事で Visual Studio CodePython の実行環境である Docker コンテナに接続して Python のプログラムが実行できるところまでは設定しましたが、Knowledge から移行データを抜くためのデータベース環境を作っていませんでした。
今回は Visual Studio CodePostgreSQL の Docker コンテナを追加します。


まずは、GooglePostgreSQL の Dockerfile の雛形を探します。ありがたいことに手順が解説されているサイトがたくさん見つかります。私は下記のサイトを参考にさせていただきました。

crudzoo.com

まず、.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 先生に頼ります。下記のようなサイトが見つかりました。

qiita.com

/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 フォルダに配置します。

f:id:tiger62shin:20220129223535p:plain

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 を起動してみます。エラーなく起動できたら PythonPostgreSQL の 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 してみましたが中身が入っているので問題ないでしょう。