今日いますぐやるべきGoogle Drive親フォルダ複数指定問題と、Google Colaboratoryを用いた探索方法
1.対処すべき課題
Google Driveにはこれまで親フォルダの「追加」機能という機能が用意されており、親フォルダを2つ以上指定することができた。この機能を用いると、それぞれの親フォルダに割り当てられた共有権限が、当該子フォルダに反映される。この性質から、複数の共有権限(例えば、部署Aと部署Bといった部署間)をまたぐファイルの共有が容易に行えた。
親フォルダと子フォルダの関係は、ストレージの常識として、1対多の関係となる。つまり、1つの親フォルダに対して複数の子フォルダが生成される関係だ。この場合、組織内のフォルダ構造はきれいなピラミッド構造になる。
Google Driveの「追加」機能を用いると、親フォルダと子フォルダの関係は多対多の関係になる。
ピラミッド構造を前提としないフォルダ構造が設計できるが、この多対多の関係性を十分に利用して設計しようとすると、その難易度は非常に高かった。
この「追加」機能は、運用難易度の関係からか、2020年10月1日に廃止が決まっている。
このため追加機能を用いたことで親フォルダが複数個存在するフォルダについて、強制的にフォルダが1つに変更される。この変更に際して、公式ブログの説明上以下のように説明されている。
私たちは 2020 年 9 月 30 日以降、ドライブ内のすべてのアイテムが親を 1 つしか持てないようにする新しいモデルに移行します。それ以外の親子関係は、元の親フォルダ内のショートカットになります。階層構造のプロパティに基づき、保持するうえで最も適切なものを親にします。
自動処理されることを待つのも一つの手だ。きっとGoogleが適切に処理してくれるのだろう。しかしながら、知らないうちにフォルダ構造が変更される状況は非常に不安定だ。従って、組織内で親フォルダが複数個設定されているフォルダがどの程度存在するか、それを探索する処理を書いたので共有しよう。
2.実行方法
新規>その他>アプリを追加からG Suite Marketplace(直リンク)を開いて、Colaboratoryをインストールする。
インストールしたColaboratoryを開いて、一番最後に記載されているコードを貼る。コード内にフォルダIDを指定する場所(以下)があるので、そこに探索したい親フォルダIDを入れよう。
「## ここに一番上のディレクトリのIDを貼り付ける
ROOT_FOLDER_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'」
実行ボタンを押すと、初回実行時においては認証ページへのURL( https://accounts.google.com/o/oauth2/〜 から始まるURL)が表示されるので、そのリンクをクリックして認証をしよう。
認証後のページに記載されているコードを、「Enter verification code:」と記載されているBoxに貼ってEnterを押すと処理が実行される。
処理実行の結果、親フォルダが2つ指定されているフォルダのリストが表示される。本日中(2020年9月30日)中に、それらのフォルダについては親フォルダを1つに直しておこう。
3.実行する処理
!pip install anytree
import json
import datetime
import time
from google.colab import auth
auth.authenticate_user()
from googleapiclient.discovery import build
drive_service = build('drive', 'v3')
from anytree import Node, RenderTree, NodeMixin
MIME_TYPE_FOLDER = 'application/vnd.google-apps.folder'
MIME_TYPE_SHORTCUT = 'application/vnd.google-apps.shortcut'
DRIVE_URL_PREFIX = 'https://drive.google.com/drive/u/0/folders/'
exec_count = []
request_count = []
next_start_time = time.time()
class BaseFolderNode(object):
pass
class FolderNode(BaseFolderNode, NodeMixin):
def __init__(self, name, id, parent_folders=[], is_shortcut=False, parent=None, children=None):
super(FolderNode, self).__init__()
self.name = name
self.id = id
self.parent_folders = parent_folders
self.is_shortcut = is_shortcut
if is_shortcut:
self.folder_type = '[ショートカット]'
else:
self.folder_type = '[フォルダ]'
self.parents_size = len(parent_folders)
self.parent = parent
if children: # set children only if given
self.children = children
def get_drive_url(id):
return DRIVE_URL_PREFIX + id
def recursive_search(parent_id, multiple_parents_folders, parent_node, exec_count, start_time, request_count):
page_token = None
child_tree = {
'folders': []
}
while True:
request_count.append(1)
now_time = time.time()
keika_time = now_time - start_time
#print(round(keika_time, 1), len(request_count))
if keika_time > 90:
if len(request_count) > 990:
time.sleep(10)
start_time = time.time()
request_count = []
if INCLUDE_SHORTCUT:
response = drive_service.files().list(q="(mimeType='" + MIME_TYPE_FOLDER + "' or mimeType='" + MIME_TYPE_SHORTCUT + "') and '" + parent_id + "' in parents",
spaces='drive', fields='files(id, name, mimeType, parents, shortcutDetails(targetId, targetMimeType))',
pageToken=page_token).execute()
else:
response = drive_service.files().list(q="(mimeType='" + MIME_TYPE_FOLDER + "') and '" + parent_id + "' in parents",
spaces='drive', fields='files(id, name, mimeType, parents)',
pageToken=page_token).execute()
children = response.get('files', [])
if children:
for folder in children:
#print(folder)
child_id = folder['id']
child_name = folder['name']
exec_count.append(1)
is_shortcut = False
if INCLUDE_SHORTCUT:
if 'shortcutDetails' in folder:
if MIME_TYPE_FOLDER == folder['shortcutDetails']['targetMimeType']:
child_id = folder['shortcutDetails']['targetId']
is_shortcut = True
else:
continue
#print(child)
parents = folder['parents']
child_node = FolderNode(child_name, child_id, parent_folders=parents, is_shortcut=is_shortcut, parent=parent_node)
multiple_parents = (len(parents) >= 2)
if multiple_parents:
multiple_parents_folders.append({'id': child_id, 'name': child_name, 'url': get_drive_url(child_id)})
child_item = {
'folder': folder,
'parents': parents,
'multipleParents': multiple_parents
}
child_tree['folders'].append(child_item)
search_result = recursive_search(child_id, multiple_parents_folders, child_node, exec_count, start_time, request_count)
if search_result['folders']:
child_tree['children'] = search_result
page_token = response.get('nextPageToken', None)
if page_token is None:
break
else:
break
return child_tree
def search_tree(root_folder_id, show_full_tree=False, exec_count=0, start_time=None, request_count=0):
root_folder = drive_service.files().get(fileId=root_folder_id).execute()
root_node = FolderNode(root_folder['name'], root_folder['id'])
multiple_parents_folders = []
tree = recursive_search(root_folder_id, multiple_parents_folders, root_node, exec_count, start_time, request_count)
dist_multiple_parents_folders = list(map(json.loads, set(map(json.dumps, multiple_parents_folders))))
#print('tree', tree)
print('フォルダ数', len(exec_count))
#print(dist_multiple_parents_folders)
if len(dist_multiple_parents_folders) > 0:
print('親フォルダを2つ持つフォルダが ' + str(len(dist_multiple_parents_folders)) + ' 個あります。')
else:
print('親フォルダを2つ持つフォルダはありませんでした。')
for pre, fill, node in RenderTree(root_node):
treestr = u"%s%s" % (pre, node.name)
if node.parents_size > 1:
print(treestr.ljust(8), get_drive_url(node.id), node.folder_type, node.parent_folders, '${mutiparents}')
else:
if show_full_tree:
print(treestr.ljust(8), get_drive_url(node.id), node.folder_type)
## ここに一番上のディレクトリのIDを貼り付ける
ROOT_FOLDER_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
# ツリーを全て表示したい場合は「True」、親フォルダを複数持つものだけに絞りたい場合「False」に書き換える
SHOW_FULL_TREE = False
# ショートカットを含む場合は「True」
INCLUDE_SHORTCUT = False
start = datetime.datetime.now()
print('開始時刻', start)
start_time = time.time()
search_tree(ROOT_FOLDER_ID, show_full_tree=SHOW_FULL_TREE, exec_count=exec_count, start_time=next_start_time, request_count=request_count)
end_time = time.time()
end = datetime.datetime.now()
print('終了時刻', end)
print('実行時間(秒)', round(end_time - start_time, 2))