見出し画像

Google Drive API v3 で社外のユーザと共有しているフォルダ/ファイルの一覧を取得してみた

TL;DR

前回の続き
マイドライブ内で リンクを知っている全員が閲覧可 もしくわ、社外のユーザに共有しているフォルダ/ファイルの一覧を取得する

コンソールに表示されるだけだと分かりづらいので、csvに書き出します。


ステップ1: 外部共有判定処理を追加する

とりあえず、レスポンスのfileオブジェクトから社外のユーザと共有している事を判定する為の項目を探します。

fileオブジェクトの中に shared という項目が存在しています。
これは共有設定されているフォルダ・ファイルの場合には true が設定されます。
しかし、この項目だけで判定してしまうと社内ユーザと共有している場合でも判定に引っかかってしまいます。
他の項目も確認してみます。

そんな中で、良さげな項目を発見しました。 permissions[] です。
この項目の説明は以下でした。

The full list of permissions for the file.
This is only available if the requesting user can share the file.
 Not populated for items in shared drives.

-- 訳
ファイルのパーミッションの完全なリストです。
これは、要求するユーザーがファイルを共有できる場合にのみ使用できます。
共有ドライブ内のアイテムには表示されません。

要求するユーザーがファイルを共有できる場合にのみ使用できるという事ですが、今回の私のケースだと問題の無い条件だったのでこの項目を使って判定してみる事にします。
リストの中身は permission オブジェクトなのでそちらの項目も確認しておきます。


type の項目を確認すると4つのタイプが存在することが分かります。

The type of the grantee. 
Valid values are:
• user
• group
• domain
• anyone
When creating a permission, if type is user or group, you must provide an emailAddress for the user or group.
When type is domain, you must provide a domain.
There isn't extra information required for a anyone type.

--訳
許可のタイプです。
有効な値は以下です。
• user
• group
• domain
• anyone
権限を作成する場合、type が user または group の場合は、 emailAddress を指定する必要があります。
タイプが domain の場合は、domain を指定する必要があります。
anyoneタイプの場合は、余分な情報は必要ありません。

ということなので、type が user もしくわ group の場合は、メールアドレスのドメイン部分が自社のメールドメイン以外かを判定する。
type が domain の場合は domain の項目をみて 自社以外のドメインかを判定する。
anyoneタイプの場合は、リンクを知っている全員 のことのようです、なので問答無用で社外共有として判定します。

ファイルには複数の permission が設定できるので、上記で1つでも判定で true だった場合には社外と共有しているファイルとします。

なので、こんな感じで判定処理を挟んでみます。

# 正規表現を使うので import する
import re 

def main():
 ~ OAuthとか諸々の処理
    results = service.files().list(
        pageSize=10, fields="nextPageToken, files(id, name, permissions(type, emailAddress, domain))").execute()
    items = results.get('files', [])

    if not items:
        print('No files found.')
    else:
        print('Files:')
        for item in items:
            if is_external_sharing(item):
                print(u'{0} ({1})'.format(item['name'], item['id']))

def is_external_sharing(item):
    for _permission in item['permissions']:
        if _permission['type'] == 'anyone':
            return True
        elif _permission['type'] == 'domain':
            if not _permission['domain'] == 'icare.jpn.com':
                return True
            else:
                continue
        elif _permission['type'] in ['user', 'group'] :
            if not re.match('.*{0}$'.format('@icare.jpn.com'), _permission['emailAddress']):
                return True
            else:
                continue

    return False

という感じで判定処理を作成したので、外部共有されているファイルだけが標準出力されるようになりました!


ステップ2: 特定のフォルダ配下のファイル一覧を取得するように変更

私のケースだと、特定のフォルダ配下で外部共有されているファイルという条件があったので、フォルダを指定する処理を追加しました。

def main():
~ OAuthとか諸々の処理
    file_id = 'aaav88RJskNSQ7JYnaaaaaa'
    results = service.files().list(
        q="'{0}' in parents and trashed = false".format(file_id),
        pageSize=10, fields="nextPageToken, files(id, name, permissions(type, emailAddress, domain))").execute()
    items = results.get('files', [])

パラメータに q を追加して、親のファイルIDを指定する事で特定フォルダ配下のファイルのみを対象に取得します。
詳しくは、以下のドキュメントに記載されています。


ステップ3: ファイル取得処理を再帰処理する

ファイル一覧を取得する際に設定している pageSize パラメータに 10 を指定しているので、10を超えるファイルを取得できません。
それに加えて、検索するフォルダ内にフォルダが存在している場合はそのフォルダ内のファイルも取得しなければならないです。
なので、再帰処理を実装して上記の問題を解決します。

このような感じで実装しました。

def main():
~ OAuthとか諸々の処理
    global service
    service = build('drive', 'v3', credentials=creds)

    root_file_id = 'aaav88RJskNSQ7JYnaaaaaa'
    search_for_external_shared_files(root_file_id)

def search_for_external_shared_files(file_id, directory = '', next_page_token = None):
    results = fetch_files(file_id, next_page_token)
    items = results.get('files', [])

    if not items:
        print('No files found.')
    else:
        for item in items:
            if is_external_sharing(item):
                print(u'{0} ({1})'.format(item['name'], item['id']))

            if is_folder(item):
                print('is folder.')
                subdirectory = directory + item['name'] + '/'
                search_for_external_shared_files(item['id'], subdirectory)

    if results.get('nextPageToken', None):
        print('Next Page.')
        search_for_external_shared_files(file_id, directory, results['nextPageToken'])

def fetch_files(file_id, next_page_token = None):
    return service.files().list(
        q="'{0}' in parents and trashed = false".format(file_id),
        fields="nextPageToken, files(id, name, mimeType, permissions(type, emailAddress, domain))",
        pageSize=10,
        pageToken=next_page_token).execute()

def is_folder(item):
    return item['mimeType'] == 'application/vnd.google-apps.folder'


要点だけ説明すると
まずは、APIクライアントの生成箇所とリクエストする箇所が切り離されてしまったので、APIクライアントをグローバル変数にします。

global service
service = build('drive', 'v3', credentials=creds)


次に、外部共有ファイルを検索する処理(search_for_external_shared_files)を切り出しました。
ついでに、リクエストだけを行う処理(fetch_files)を切り出していますが、そちらはご自由にしてください。

レスポンスオブジェクトに nextPageToken が存在するかを判定します。
ここに値が設定されていた場合には、まだ取得できていないファイルが存在しています。
その場合は nextPageToken を使ってリクエストを行わないといけないので、search_for_external_shared_files  の引数に設定しておきます。

if results.get('nextPageToken', None):
    print('Next Page.')
    search_for_external_shared_files(file_id, results['nextPageToken'])


リクエストのパラメータに pageToken を追加します。
設定する値は先程の nextPageToken  です。
上手く再帰処理が動作する事を確認する為に pageSize を 10 のままにしていますが変更して問題ないです。
あとフォルダ判定の為に mimeType を取得するフィールドに追加してます。

def fetch_files(file_id, next_page_token = None):
    return service.files().list(
        q="'{0}' in parents and trashed = false".format(file_id),
        fields="nextPageToken, files(id, name, mimeType, permissions(type, emailAddress, domain))",
        pageSize=10,
        pageToken=next_page_token).execute()


フォルダ判定の為に is_folder の判定処理の作成と、そちらでの再帰処理も実装しています。
必要なのかは微妙ですが、フォルダの階層を保持するために引数(directory)を引き回しています。

def is_folder(item):
    return item['mimeType'] == 'application/vnd.google-apps.folder'
if is_folder(item):
    print('is folder.')
    subdirectory = directory + item['name'] + '/'
    search_for_external_shared_files(item['id'], subdirectory)


ステップ4: csv出力を実装する

まず、csvのモジュールをimportします

import csv


csvはExcelで開いても文字化けしないように utf-8 の bom付きで出力するようにします。

def main():
~ OAuthとか諸々の処理
    global writer
    f = open('external_shared_files.csv', 'w', encoding="utf_8_sig")
    writer = csv.writer(f, lineterminator='\n')


あとは、標準出力していた箇所を csvの書き出し処理に変更するだけです。

if is_external_sharing(item):
    writer.writerow([item['id'], directory, item['name'], item['mimeType']])

出力する項目はお好みで変更してください。


まとめ

公式のドキュメントがしっかり記載されているので特に迷うことなく作成することができました。

もしかしたら、ソース全体を確認したい方がおられるかもしれないので一応 github のリンクを貼っておきます。


この記事が気に入ったらサポートをしてみませんか?