From ac5dc544db8463b115d4da43547e196f7da5ed4f Mon Sep 17 00:00:00 2001 From: TomBaxter Date: Mon, 2 Oct 2017 16:52:28 -0400 Subject: [PATCH 1/3] Update GoogleDrive provider to use GD v3 API [SVCS-426 ] GoogleDrive migration guide https://developers.google.com/drive/v3/web/migration Of particular note: GD v3 API has no method of downloading GoogleDoc revisions. This PR leaves behind GD v2 calls, in order to maintain this functionality as long as possible. GD v3 returns minimal representations of resources. Fields must be specified, to be returned. Many field names changed. Most commonly in the provider: title -> name modifiedDate -> modifiedTime fileSize -> size etags are no longer available from GoogleDrive exportLinks are no longer available in the 'file' representation of GoogleDoc files. GD v3 returns lists of resources as either 'files' or 'revisions' as opposed to GD v2 which returned 'items' for all resource lists. --- .../googledrive/fixtures/revisions.json | 36 +- .../googledrive/fixtures/root_provider.json | 386 ++++------- .../googledrive/fixtures/sharing.json | 599 +++++----------- tests/providers/googledrive/test_metadata.py | 100 +-- tests/providers/googledrive/test_provider.py | 644 ++++++++++-------- waterbutler/providers/googledrive/metadata.py | 49 +- waterbutler/providers/googledrive/provider.py | 245 ++++--- waterbutler/providers/googledrive/settings.py | 4 +- waterbutler/providers/googledrive/utils.py | 48 +- 9 files changed, 930 insertions(+), 1181 deletions(-) diff --git a/tests/providers/googledrive/fixtures/revisions.json b/tests/providers/googledrive/fixtures/revisions.json index 3692c8baf..093c57914 100644 --- a/tests/providers/googledrive/fixtures/revisions.json +++ b/tests/providers/googledrive/fixtures/revisions.json @@ -1,31 +1,25 @@ { "revision_metadata": { - "modifiedDate": "2015-01-01T16:54:58.929Z", - "md5Checksum": "43c5a01efeaea6bfd0433fa516a0d71f", - "fileSize": "918668", - "lastModifyingUser": {"kind": "drive#user", "permissionId": "07992110234966807597", "emailAddress": "jm.carp@gmail.com", "isAuthenticatedUser": true, "displayName": "Joshua Carp", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}}, - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/revisions/1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA", - "lastModifyingUserName": "Joshua Carp", - "downloadUrl": "https://doc-0g-5k-docs.googleusercontent.com/docs/securesc/6l6ti67c1gnej8b4rr55nfimce1282lr/4kr23qf2mtt0g577quhmb7sn41q8t9kl/1424894400000/07992110234966807597/07992110234966807597/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR?rid=1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA&e=download&gd=true", + "id": "0BxdmnuT5XpqcOGNTWDl3K044Yi83ZU14aDBzSThoRFJEb25jPQ", "mimeType": "application/pdf", - "id": "1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA", - "kind": "drive#revision", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/58C3fd_BFF2TTwuUg7RCveoALHM\"", - "published": false, - "pinned": false + "modifiedTime": "2017-10-12T13:31:17.922Z", + "originalFilename": "xps-13-9343-laptop_Service Manual_en-us.pdf", + "md5Checksum": "53141aa6a986e636bcdcef2ff08beece", + "size": "8604865" }, "revisions_list": { - "kind": "drive#revisionList", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/PeANBe5F3yk-YAzsoQO4pYPA5W8\"", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/revisions", - "items": [ - {"selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/revisions/1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA", "md5Checksum": "43c5a01efeaea6bfd0433fa516a0d71f", "pinned": false, "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/sdQBhqMbZWHR5JnnXwR2jMjJYa4\"", "id": "1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA", "kind": "drive#revision", "lastModifyingUserName": "Joshua Carp", "mimeType": "application/pdf", "fileSize": "918668", "lastModifyingUser": {"kind": "drive#user", "emailAddress": "jm.carp@gmail.com", "permissionId": "07992110234966807597", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}, "displayName": "Joshua Carp", "isAuthenticatedUser": true}, "downloadUrl": "https://doc-0g-5k-docs.googleusercontent.com/docs/securesc/6l6ti67c1gnej8b4rr55nfimce1282lr/nccdohm8rrrqmabek0d1ot5alel2768g/1424959200000/07992110234966807597/07992110234966807597/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR?rid=1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA&e=download&gd=true", "modifiedDate": "2015-01-01T16:54:58.929Z", "published": false} + "revisions": [ + { + "id": "0BxdmnuT5XpqcdFdzM2dtTnB4bnNuUXpmYlZkOUZDV1AvcWFRPQ", + "mimeType": "image/jpeg", + "modifiedTime": "2017-10-16T13:27:10.965Z", + "originalFilename": "sunshine.jpg", + "md5Checksum": "2f7be16736648476f22f08e2e3978d08", + "size": "3782" + } ] }, "revisions_list_empty":{ - "kind": "drive#revisionList", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/PeANBe5F3yk-YAzsoQO4pYPA5W8\"", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/revisions", - "items": [] + "revisions": [] } } diff --git a/tests/providers/googledrive/fixtures/root_provider.json b/tests/providers/googledrive/fixtures/root_provider.json index 55999fc29..86347f39e 100644 --- a/tests/providers/googledrive/fixtures/root_provider.json +++ b/tests/providers/googledrive/fixtures/root_provider.json @@ -1,292 +1,140 @@ { "docs_file_metadata": { - "modifiedByMeDate": "2015-02-23T18:00:56.343Z", + "id": "1nuKOTbIo0CD9MVTUAGmBQ-RUv_kv8x7zJjLo_juUByw", + "name": "version-test", "mimeType": "application/vnd.google-apps.document", - "lastModifyingUser": { - "emailAddress": "jm.carp@gmail.com", - "displayName": "Joshua Carp", - "isAuthenticatedUser": true, - "permissionId": "07992110234966807597", - "picture": { - "url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg" - }, - "kind": "drive#user" - }, - "editable": true, - "quotaBytesUsed": "0", - "embedLink": "https://docs.google.com/document/d/12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ/preview", - "writersCanShare": true, - "kind": "drive#file", - "selfLink": "https://www.googleapis.com/drive/v2/files/12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ", - "markedViewedByMeDate": "1970-01-01T00:00:00.000Z", - "alternateLink": "https://docs.google.com/document/d/12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ/edit?usp=drivesdk", - "copyable": true, - "title": "version-test", - "shared": false, - "lastViewedByMeDate": "2015-02-25T14:37:45.764Z", - "parents": [{"id": "0ABtc_QrXguAwUk9PVA", "isRoot": true, "parentLink": "https://www.googleapis.com/drive/v2/files/0ABtc_QrXguAwUk9PVA", "selfLink": "https://www.googleapis.com/drive/v2/files/12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ/parents/0ABtc_QrXguAwUk9PVA", "kind": "drive#parentReference"}], - "iconLink": "https://ssl.gstatic.com/docs/doclist/images/icon_11_document_list.png", - "appDataContents": false, - "version": "171431", - "owners": [{"emailAddress": "jm.carp@gmail.com", "displayName": "Joshua Carp", "isAuthenticatedUser": true, "permissionId": "07992110234966807597", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}, "kind": "drive#user"}], - "userPermission": { - "id": "me", - "type": "user", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/HzagE4wEPKWbXBmdARETS4UMfLw\"", - "selfLink": "https://www.googleapis.com/drive/v2/files/12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ/permissions/me", - "kind": "drive#permission", - "role": "owner" - }, - "exportLinks": { - "application/rtf": "https://docs.google.com/feeds/download/documents/export/Export?id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&exportFormat=rtf", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "https://docs.google.com/feeds/download/documents/export/Export?id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&exportFormat=docx", - "application/vnd.oasis.opendocument.text": "https://docs.google.com/feeds/download/documents/export/Export?id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&exportFormat=odt", "text/html": "https://docs.google.com/feeds/download/documents/export/Export?id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&exportFormat=html", "application/pdf": "https://docs.google.com/feeds/download/documents/export/Export?id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&exportFormat=pdf", - "text/plain": "https://docs.google.com/feeds/download/documents/export/Export?id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&exportFormat=txt" - }, - "id": "12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ", - "modifiedDate": "2015-02-23T18:00:56.343Z", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MTQyNDcxNDQ1NjM0Mw\"", - "ownerNames": ["Joshua Carp"], - "thumbnailLink": "https://docs.google.com/feeds/vt?gd=true&id=12go3hvnFZN5IuMWtz-2dft4EcPa5ti21pQYH1rjcxfQ&v=0&s=AMedNnoAAAAAVO5G_Xav-rLfEJkrZvp0L9OQpE345Z-k&sz=s220", - "labels": {"hidden": false, "trashed": false, "starred": false, "viewed": true, "restricted": false}, - "createdDate": "2015-02-23T17:59:47.913Z", - "lastModifyingUserName": "Joshua Carp" - }, - "list_file_empty": { - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MrSD7Al_zGPN4CKisJpWLDC3cyY\"", - "kind": "drive#fileList", - "items": [], - "selfLink": "https://www.googleapis.com/drive/v2/files?q=\"0ABtc_QrXguAwUk9PVA\"+in+parents+and+trashed+%3D+false+and+title+%3D+\"PART_1420130849837.pdf\"&alt=json" + "version": "6", + "webViewLink": "https://docs.google.com/document/d/1nuKOTbIo0CD9MVTUAGmBQ-RUv_kv8x7zJjLo_juUByw/edit?usp=drivesdk", + "createdTime": "2017-10-12T14:16:07.834Z", + "modifiedTime": "2017-10-12T14:16:38.186Z", + "capabilities": { + "canCopy": true, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": true, + "canRename": true, + "canShare": true, + "canTrash": true + } }, "folder_metadata":{ - "id": "0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA", - "createdDate": "2015-02-20T21:04:44.895Z", - "modifiedDate": "2015-02-20T21:04:44.812Z", - "copyable": false, - "labels": {"trashed": false, "viewed": true, "restricted": false, "starred": false, "hidden": false}, - "editable": true, - "quotaBytesUsed": "0", - "lastModifyingUserName": "Joshua Carp", - "writersCanShare": true, - "version": "171093", - "lastModifyingUser": {"kind": "drive#user", "permissionId": "07992110234966807597", "emailAddress": "jm.carp@gmail.com", "isAuthenticatedUser": true, "displayName": "Joshua Carp", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}}, - "kind": "drive#file", - "lastViewedByMeDate": "2015-02-22T20:43:20.265Z", - "owners": [{"kind": "drive#user", "permissionId": "07992110234966807597", "emailAddress": "jm.carp@gmail.com", "isAuthenticatedUser": true, "displayName": "Joshua Carp", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}}], - "appDataContents": false, - "markedViewedByMeDate": "1970-01-01T00:00:00.000Z", - "ownerNames": ["Joshua Carp"], - "alternateLink": "https://docs.google.com/folderview?id=0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA&usp=drivesdk", - "shared": false, - "iconLink": "https://ssl.gstatic.com/docs/doclist/images/icon_11_collection_list.png", - "selfLink": "https://www.googleapis.com/drive/v2/files/0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA", - "modifiedByMeDate": "2015-02-20T21:04:44.812Z", - "title": "osf test", + "id": "0BxdmnuT5XpqcM1NYT0hpLVptLW8", + "name": "osf test", "mimeType": "application/vnd.google-apps.folder", - "userPermission": {"kind": "drive#permission", "selfLink": "https://www.googleapis.com/drive/v2/files/0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA/permissions/me", "type": "user", "id": "me", "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/UD2uob-gKfBZit-qg5BrpgLMl4U\"", "role": "owner"}, - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MTQyNDQ2NjI4NDgxMg\"", - "parents": [{"parentLink": "https://www.googleapis.com/drive/v2/files/0ABtc_QrXguAwUk9PVA", "kind": "drive#parentReference", "id": "0ABtc_QrXguAwUk9PVA", "isRoot": true, "selfLink": "https://www.googleapis.com/drive/v2/files/0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA/parents/0ABtc_QrXguAwUk9PVA"}] + "version": "1", + "webViewLink": "https://drive.google.com/drive/folders/0BxdmnuT5XpqcM1NYT0hpLVptLW8", + "createdTime": "2017-10-12T14:23:22.939Z", + "modifiedTime": "2017-10-12T14:23:22.939Z", + "capabilities": { + "canCopy": false, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": false, + "canRename": true, + "canShare": true, + "canTrash": true + } + }, + "folder2_metadata":{ + "id": "0BxdmnuT5XpqcM1NYT0hpLVptLX9", + "name": "osf test2", + "mimeType": "application/vnd.google-apps.folder", + "version": "1", + "webViewLink": "https://drive.google.com/drive/folders/0BxdmnuT5XpqcM1NYT0hpLVptLW8", + "createdTime": "2017-10-12T13:23:22.939Z", + "modifiedTime": "2017-10-12T13:23:22.939Z", + "capabilities": { + "canCopy": false, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": false, + "canRename": true, + "canShare": true, + "canTrash": true + } }, "folder_metadata_forward_slash": { - "id": "0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA", - "createdDate": "2015-02-20T21:04:44.895Z", - "modifiedDate": "2015-02-20T21:04:44.812Z", - "copyable": false, - "labels": {"trashed": false, "viewed": true, "restricted": false, "starred": false, "hidden": false}, - "editable": true, - "quotaBytesUsed": "0", - "lastModifyingUserName": "Joshua Carpe", - "writersCanShare": true, - "version": "171093", - "lastModifyingUser": {"kind": "drive#user", "permissionId": "07992110234966807597", "emailAddress": "jm.carp@gmail.com", "isAuthenticatedUser": true, "displayName": "Joshua Carp", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}}, - "kind": "drive#file", - "lastViewedByMeDate": "2015-02-22T20:43:20.265Z", - "owners": [{"kind": "drive#user", "permissionId": "07992110234966807597", "emailAddress": "jm.carp@gmail.com", "isAuthenticatedUser": true, "displayName": "Joshua Carp", "picture": {"url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg"}}], - "appDataContents": false, - "markedViewedByMeDate": "1970-01-01T00:00:00.000Z", - "ownerNames": ["Joshua Carp"], - "alternateLink": "https://docs.google.com/folderview?id=0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA&usp=drivesdk", - "shared": false, - "iconLink": "https://ssl.gstatic.com/docs/doclist/images/icon_11_collection_list.png", - "selfLink": "https://www.googleapis.com/drive/v2/files/0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA", - "modifiedByMeDate": "2015-02-20T21:04:44.812Z", - "title": "osf/test", + "id": "0BxdmnuT5XpqcM1NYT0hpLVptLW8", + "name": "test/Folder", "mimeType": "application/vnd.google-apps.folder", - "userPermission": {"kind": "drive#permission", "selfLink": "https://www.googleapis.com/drive/v2/files/0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA/permissions/me", "type": "user", "id": "me", "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/UD2uob-gKfBZit-qg5BrpgLMl4U\"", "role": "owner"}, - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MTQyNDQ2NjI4NDgxMg\"", - "parents": [{"parentLink": "https://www.googleapis.com/drive/v2/files/0ABtc_QrXguAwUk9PVA", "kind": "drive#parentReference", "id": "0ABtc_QrXguAwUk9PVA", "isRoot": true, "selfLink": "https://www.googleapis.com/drive/v2/files/0Bxtc_QrXguAwfmpVRE5vOGpQNVhJZ3F5ZHEwbjBid0lXaFUyQ3psaDltWENiVVNOaEdTNzA/parents/0ABtc_QrXguAwUk9PVA"}] + "version": "1", + "webViewLink": "https://drive.google.com/drive/folders/0BxdmnuT5XpqcM1NYT0hpLVptLW8", + "createdTime": "2017-10-12T14:23:22.939Z", + "modifiedTime": "2017-10-12T14:23:22.939Z", + "capabilities": { + "canCopy": false, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": false, + "canRename": true, + "canShare": true, + "canTrash": true + } }, - "list_file":{ - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MrSD7Al_zGPN4CKisJpWLDC3cyY\"", - "kind": "drive#fileList", - "items": [ + "list_file": { + "files": [ { - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MTQyMDEzMTI5ODkyOQ\"", - "owners": [ - { - "picture": { - "url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg" - }, - "kind": "drive#user", - "permissionId": "07992110234966807597", - "displayName": "Joshua Carp", - "emailAddress": "jm.carp@gmail.com", - "isAuthenticatedUser": true - } - ], - "parents": [ - { - "parentLink": "https://www.googleapis.com/drive/v2/files/0ABtc_QrXguAwUk9PVA", - "isRoot": true, - "kind": "drive#parentReference", - "id": "0ABtc_QrXguAwUk9PVA", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/parents/0ABtc_QrXguAwUk9PVA" - } - ], - "ownerNames": [ - "Joshua Carp" - ], - "downloadUrl": "https://doc-0g-5k-docs.googleusercontent.com/docs/securesc/6l6ti67c1gnej8b4rr55nfimce1282lr/0sr833dlhh6p7ukpt3f4940se41ornad/1424880000000/07992110234966807597/07992110234966807597/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR?e=download&gd=true", - "writersCanShare": true, - "title": "PART_1420130849837.pdf", - "editable": true, - "lastModifyingUser": { - "picture": { - "url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg" - }, - "kind": "drive#user", - "permissionId": "07992110234966807597", - "displayName": "Joshua Carp", - "emailAddress": "jm.carp@gmail.com", - "isAuthenticatedUser": true - }, - "quotaBytesUsed": "918668", - "mimeType": "application/pdf", - "createdDate": "2015-01-01T16:54:58.929Z", - "alternateLink": "https://docs.google.com/file/d/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/edit?usp=drivesdk", - "headRevisionId": "1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA", - "id": "1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR", - "modifiedDate": "2015-01-01T16:54:58.929Z", - "kind": "drive#file", - "fileExtension": "pdf", - "iconLink": "https://ssl.gstatic.com/docs/doclist/images/icon_11_pdf_list.png", - "appDataContents": false, - "lastModifyingUserName": "Joshua Carp", - "webContentLink": "https://docs.google.com/uc?id=1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR&export=download", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR", - "copyable": true, - "fileSize": "918668", - "labels": { - "viewed": false, - "trashed": false, - "restricted": false, - "hidden": false, - "starred": false - }, - "version": "143933", - "md5Checksum": "6b50249f91258397fc5cb7d5a4127e15", - "userPermission": { - "type": "user", - "kind": "drive#permission", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/2RVviHZ60Y5d9plBp5dBdmj2r70\"", - "id": "me", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/permissions/me", - "role": "owner" - }, - "shared": false, - "markedViewedByMeDate": "1970-01-01T00:00:00.000Z" - } - ], - "selfLink": "https://www.googleapis.com/drive/v2/files?q=\"0ABtc_QrXguAwUk9PVA\"+in+parents+and+trashed+%3D+false+and+title+%3D+\"PART_1420130849837.pdf\"&alt=json" + "id": "0BxdmnuT5XpqcUXZwRzZ2TWZzUm8", + "name": "xps-13-9343-laptop_Service Manual_en-us.pdf", + "mimeType": "application/pdf", + "version": "3", + "webContentLink": "https://drive.google.com/uc?id=0BxdmnuT5XpqcUXZwRzZ2TWZzUm8&export=download", + "webViewLink": "https://drive.google.com/file/d/0BxdmnuT5XpqcUXZwRzZ2TWZzUm8/view?usp=drivesdk", + "createdTime": "2017-10-12T13:31:17.922Z", + "modifiedTime": "2017-10-12T13:31:17.922Z", + "capabilities": { + "canCopy": true, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": true, + "canRename": true, + "canShare": true, + "canTrash": true + }, + "md5Checksum": "6b50249f91258397fc5cb7d5a4127e15", + "size": "8604865" + } + ] }, "file_forward_slash": { - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MTQyMDEzMTI5ODkyOQ\"", - "owners": [ - { - "picture": { - "url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg" - }, - "kind": "drive#user", - "permissionId": "07992110234966807597", - "displayName": "Joshua Carp", - "emailAddress": "jm.carp@gmail.com", - "isAuthenticatedUser": true - } - ], - "parents": [ - { - "parentLink": "https://www.googleapis.com/drive/v2/files/0ABtc_QrXguAwUk9PVA", - "isRoot": true, - "kind": "drive#parentReference", - "id": "0ABtc_QrXguAwUk9PVA", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/parents/0ABtc_QrXguAwUk9PVA" - } - ], - "ownerNames": [ - "Joshua Carpe" - ], - "downloadUrl": "https://doc-0g-5k-docs.googleusercontent.com/docs/securesc/6l6ti67c1gnej8b4rr55nfimce1282lr/0sr833dlhh6p7ukpt3f4940se41ornad/1424880000000/07992110234966807597/07992110234966807597/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR?e=download&gd=true", - "writersCanShare": true, - "title": "PART_1420130849837.pdf", - "editable": true, - "lastModifyingUser": { - "picture": { - "url": "https://lh3.googleusercontent.com/-ndG-yHyqonM/AAAAAAAAAAI/AAAAAAAAADs/wUR8YhDe3vY/s64/photo.jpg" - }, - "kind": "drive#user", - "permissionId": "07992110234966807597", - "displayName": "Joshua Carp", - "emailAddress": "jm.carp@gmail.com", - "isAuthenticatedUser": true - }, - "quotaBytesUsed": "918668", - "mimeType": "application/pdf", - "createdDate": "2015-01-01T16:54:58.929Z", - "alternateLink": "https://docs.google.com/file/d/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/edit?usp=drivesdk", - "headRevisionId": "1DVR6FVQGOSpUrtHjxCKb4-2R0chGVJFG6wVPQwq1o-gay_tqwA", - "id": "1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR", - "modifiedDate": "2015-01-01T16:54:58.929Z", - "kind": "drive#file", - "fileExtension": "pdf", - "iconLink": "https://ssl.gstatic.com/docs/doclist/images/icon_11_pdf_list.png", - "appDataContents": false, - "lastModifyingUserName": "Joshua Carp", - "webContentLink": "https://docs.google.com/uc?id=1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR&export=download", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR", - "copyable": true, - "fileSize": "918668", - "labels": { - "viewed": false, - "trashed": false, - "restricted": false, - "hidden": false, - "starred": false - }, - "version": "143933", - "md5Checksum": "43c5a01efeaea6bfd0433fa516a0d71f", - "userPermission": { - "type": "user", - "kind": "drive#permission", - "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/2RVviHZ60Y5d9plBp5dBdmj2r70\"", - "id": "me", - "selfLink": "https://www.googleapis.com/drive/v2/files/1GwpK7IozbO01RiyC5aPd66v7ShEViqggvT6ur5_pZMFo-ZzQHOgkyoU3ztjf0ytKt0HSdvUg6O2nmoYR/permissions/me", - "role": "owner" - }, - "shared": false, - "markedViewedByMeDate": "1970-01-01T00:00:00.000Z" + "id": "0BxdmnuT5XpqcUXZwRzZ2TWZzUm8", + "name": "xps-13-9343-lap/top_Service Manual_en-us.pdf", + "mimeType": "application/pdf", + "version": "3", + "webContentLink": "https://drive.google.com/uc?id=0BdmnuT5XpqUXZwRzZ2TWZzUm8&export=doad", + "webViewLink": "https://drive.google.com/file/d/0BdmnuT5XpqUXZwRzZ2TWZzUm8/view?usp=", + "createdTime": "2017-10-12T13:31:17.922Z", + "modifiedTime": "2017-10-12T13:31:17.922Z", + "capabilities": { + "canCopy": true, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": true, + "canRename": true, + "canShare": true, + "canTrash": true + }, + "md5Checksum": "53141aa6a986e636bcdcef2ff08beece", + "size": "8604865" }, - "delete_contents_metadata": {"items": [{"id": "0BwZjL1zsLzNbY0E4b2xtaFRLa2M"}]}, + "delete_contents_metadata": {"files": [{"id": "0BwZjL1zsLzNbY0E4b2xtaFRLa2M"}]}, - "revalidate_path_file_metadata_1": {"items": [{"id": "0BwZjL1zsLzNbRFpibmVRcW9ueFE"}]}, + "revalidate_path_file_metadata_1": {"files": [{"id": "0BwZjL1zsLzNbRFpibmVRcW9ueFE"}]}, - "revalidate_path_file_metadata_2": {"title": "Gear1.stl", "id": "0BwZjL1zsLzNbRFpibmVRcW9ueFE", "mimeType": "application/vnd.ms-pki.stl"}, + "revalidate_path_file_metadata_2": {"name": "Gear1.stl", "id": "0BwZjL1zsLzNbRFpibmVRcW9ueFE", "mimeType": "application/vnd.ms-pki.stl"}, - "revalidate_path_gdoc_file_metadata": {"title": "Gear1.gdoc", "id": "0BwZjL1zsLzNbRFpibmVRcW9ueFE", "mimeType": "application/vnd.ms-pki.stl"}, + "revalidate_path_gdoc_file_metadata": {"name": "Gear1.gdoc", "id": "0BwZjL1zsLzNbRFpibmVRcW9ueFE", "mimeType": "application/vnd.ms-pki.stl"}, - "revalidate_path_folder_metadata_1": {"items": [{"id": "0BwZjL1zsLzNbNG5zVzJFYXF4bm8"}]}, + "revalidate_path_folder_metadata_1": {"files": [{"id": "0BwZjL1zsLzNbNG5zVzJFYXF4bm8"}]}, - "revalidate_path_folder_metadata_2": {"title": "inception folder yo", "id": "0BwZjL1zsLzNbNG5zVzJFYXF4bm8", "mimeType": "application/vnd.google-apps.folder"}, + "revalidate_path_folder_metadata_2": {"name": "inception folder yo", "id": "0BwZjL1zsLzNbNG5zVzJFYXF4bm8", "mimeType": "application/vnd.google-apps.folder"}, "checksum_mismatch_metadata": { "etag": "\"zWM2D6PBtLRQKuDNbaQNSNEy5BE/MTQyMDEzMTI5ODkyOQ\"", diff --git a/tests/providers/googledrive/fixtures/sharing.json b/tests/providers/googledrive/fixtures/sharing.json index 580190073..fd60779d9 100644 --- a/tests/providers/googledrive/fixtures/sharing.json +++ b/tests/providers/googledrive/fixtures/sharing.json @@ -1,153 +1,129 @@ { "editable_gdoc": { "metadata": { - "iconLink" : "https://drive-thirdparty.googleusercontent.com/16/type/application/vnd.google-apps.document", - "version" : "43684", - "createdDate" : "2017-02-24T18:36:58.044Z", - "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ", - "lastModifyingUserName" : "Fitz Elliott", - "appDataCqontents" : false, - "explicitlyTrashed" : false, - "exportLinks" : { - "application/vnd.oasis.opendocument.text" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=odt", - "application/zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=zip", - "text/html" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=html", - "application/pdf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=pdf", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=docx", - "text/plain" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=txt", - "application/rtf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=rtf", - "application/epub+zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=epub" - }, - "alternateLink" : "https://docs.google.com/a/cos.io/document/d/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/edit?usp=drivesdk", - "modifiedDate" : "2017-02-24T18:37:28.569Z", - "thumbnailLink" : "https://docs.google.com/a/cos.io/feeds/vt?gd=true&id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&v=1&s=AMedNnoAAAAAWRo2ctWxLS7eQmcAhSbHbBAhdQmkSbVa&sz=s220", - "id" : "1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ", - "spaces" : [ - "drive" - ], - "mimeType" : "application/vnd.google-apps.document", - "userPermission" : { - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/Q8v9eUEXTjEPR7_OHywQXTXv8d0\"", - "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/permissions/me", - "role" : "owner", - "id" : "me", - "type" : "user", - "kind" : "drive#permission" - }, - "title" : "editable_gdoc", - "capabilities" : { - "canEdit" : true, - "canCopy" : true - }, - "lastModifyingUser" : { - "kind" : "drive#user", - "isAuthenticatedUser" : true, - "emailAddress" : "fitz@cos.io", - "displayName" : "Fitz Elliott", - "permissionId" : "17667957036718347700" - }, - "kind" : "drive#file", - "editable" : true, - "shared" : false, - "copyable" : true, - "markedViewedByMeDate" : "1970-01-01T00:00:00.000Z", - "writersCanShare" : true, - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/MTQ4Nzk2MTQ0ODU2OQ\"", - "modifiedByMeDate" : "2017-02-24T18:37:28.569Z", - "labels" : { - "restricted" : false, - "hidden" : false, - "starred" : false, - "trashed" : false, - "viewed" : true - }, - "owners" : [ - { - "emailAddress" : "fitz@cos.io", - "displayName" : "Fitz Elliott", - "permissionId" : "17667957036718347700", - "isAuthenticatedUser" : true, - "kind" : "drive#user" - } - ], - "parents" : [ - { - "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/parents/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "parentLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "isRoot" : false, - "id" : "0B74RCNS4TbRVNGJwLWFacnlKdGM", - "kind" : "drive#parentReference" - } - ], - "quotaBytesUsed" : "0", - "lastViewedByMeDate" : "2017-02-24T18:37:28.569Z", - "embedLink" : "https://docs.google.com/a/cos.io/document/d/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/preview", - "ownerNames" : [ - "Fitz Elliott" - ] + "id": "1B95iwzZnU032YUFvyl8kjVnx-eGlJz6FrIRt-DDxQKw", + "name": "editable_gdoc", + "mimeType": "application/vnd.google-apps.document", + "webViewLink": "https://docs.google.com/document/d/1B95iwzZnU032YUFvyl8kjVnx-eGlJz6FrIRt-DDxQKw/edit?usp=drivesdk", + "createdTime": "2017-10-13T17:07:04.259Z", + "modifiedTime": "2017-10-13T17:07:13.850Z", + "capabilities": { + "canCopy": true, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": true, + "canRename": true, + "canShare": true, + "canTrash": true + } }, + "v2_metadata": { + "iconLink" : "https://drive-thirdparty.googleusercontent.com/16/type/application/vnd.google-apps.document", + "version" : "43684", + "createdDate" : "2017-02-24T18:36:58.044Z", + "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ", + "lastModifyingUserName" : "Fitz Elliott", + "appDataCqontents" : false, + "explicitlyTrashed" : false, + "exportLinks" : { + "application/vnd.oasis.opendocument.text" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=odt", + "application/zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=zip", + "text/html" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=html", + "application/pdf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=docx", + "text/plain" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=txt", + "application/rtf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=rtf", + "application/epub+zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&exportFormat=epub" + }, + "alternateLink" : "https://docs.google.com/a/cos.io/document/d/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/edit?usp=drivesdk", + "modifiedDate" : "2017-02-24T18:37:28.569Z", + "thumbnailLink" : "https://docs.google.com/a/cos.io/feeds/vt?gd=true&id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&v=1&s=AMedNnoAAAAAWRo2ctWxLS7eQmcAhSbHbBAhdQmkSbVa&sz=s220", + "id" : "1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ", + "spaces" : [ + "drive" + ], + "mimeType" : "application/vnd.google-apps.document", + "userPermission" : { + "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/Q8v9eUEXTjEPR7_OHywQXTXv8d0\"", + "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/permissions/me", + "role" : "owner", + "id" : "me", + "type" : "user", + "kind" : "drive#permission" + }, + "title" : "editable_gdoc", + "capabilities" : { + "canEdit" : true, + "canCopy" : true + }, + "lastModifyingUser" : { + "kind" : "drive#user", + "isAuthenticatedUser" : true, + "emailAddress" : "fitz@cos.io", + "displayName" : "Fitz Elliott", + "permissionId" : "17667957036718347700" + }, + "kind" : "drive#file", + "editable" : true, + "shared" : false, + "copyable" : true, + "markedViewedByMeDate" : "1970-01-01T00:00:00.000Z", + "writersCanShare" : true, + "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/MTQ4Nzk2MTQ0ODU2OQ\"", + "modifiedByMeDate" : "2017-02-24T18:37:28.569Z", + "labels" : { + "restricted" : false, + "hidden" : false, + "starred" : false, + "trashed" : false, + "viewed" : true + }, + "owners" : [ + { + "emailAddress" : "fitz@cos.io", + "displayName" : "Fitz Elliott", + "permissionId" : "17667957036718347700", + "isAuthenticatedUser" : true, + "kind" : "drive#user" + } + ], + "parents" : [ + { + "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/parents/0B74RCNS4TbRVNGJwLWFacnlKdGM", + "parentLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVNGJwLWFacnlKdGM", + "isRoot" : false, + "id" : "0B74RCNS4TbRVNGJwLWFacnlKdGM", + "kind" : "drive#parentReference" + } + ], + "quotaBytesUsed" : "0", + "lastViewedByMeDate" : "2017-02-24T18:37:28.569Z", + "embedLink" : "https://docs.google.com/a/cos.io/document/d/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/preview", + "ownerNames" : [ + "Fitz Elliott" + ] + }, "revisions": { - "items" : [ + "revisions": [ { - "exportLinks" : { - "application/rtf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=rtf", - "application/epub+zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=epub", - "application/zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=zip", - "application/pdf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=pdf", - "text/plain" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=txt", - "application/vnd.oasis.opendocument.text" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=odt", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=docx", - "text/html" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=1&exportFormat=html" - }, - "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/revisions/1", - "id" : "1", - "kind" : "drive#revision", - "etag" : "\"LUxk1DXE_0fd4yeJDIgpecr5uPA/lHudfDwfTEz0RxswkJ6vUFpP4z8\"", - "modifiedDate" : "2017-02-24T18:36:58.041Z", - "lastModifyingUser" : { - "emailAddress" : "fitz@cos.io", - "kind" : "drive#user", - "permissionId" : "17667957036718347700", - "isAuthenticatedUser" : true, - "displayName" : "Fitz Elliott" - }, - "lastModifyingUserName" : "Fitz Elliott", - "mimeType" : "application/vnd.google-apps.document", - "published" : false + "id": "1", + "mimeType": "application/vnd.google-apps.document", + "modifiedTime": "2017-10-13T17:07:04.259Z" }, { - "exportLinks" : { - "application/vnd.oasis.opendocument.text" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=odt", - "text/html" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=html", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=docx", - "application/pdf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=pdf", - "text/plain" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=txt", - "application/zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=zip", - "application/epub+zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=epub", - "application/rtf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ&revision=11&exportFormat=rtf" - }, - "id" : "11", - "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/revisions/11", - "kind" : "drive#revision", - "etag" : "\"LUxk1DXE_0fd4yeJDIgpecr5uPA/EYC91bmPetPKWKMdh9HrqjoYm7A\"", - "modifiedDate" : "2017-02-24T18:37:28.552Z", - "lastModifyingUser" : { - "kind" : "drive#user", - "emailAddress" : "fitz@cos.io", - "displayName" : "Fitz Elliott", - "isAuthenticatedUser" : true, - "permissionId" : "17667957036718347700" - }, - "lastModifyingUserName" : "Fitz Elliott", - "published" : false, - "mimeType" : "application/vnd.google-apps.document" + "id": "9", + "mimeType": "application/vnd.google-apps.document", + "modifiedTime": "2017-10-13T17:07:13.819Z" } - ], - "kind" : "drive#revisionList", - "etag" : "\"LUxk1DXE_0fd4yeJDIgpecr5uPA/U3foOOt7M1OyjEO9G9omVQ4w4G4\"", - "selfLink" : "https://www.googleapis.com/drive/v2/files/1XeE-iK-SolhmYntE5gq9fVT_icHrJ2hTIEISZ6t4COQ/revisions" + ] }, "revision": { + "id": "1", + "mimeType": "application/vnd.google-apps.document", + "modifiedTime": "2017-10-13T17:07:04.259Z" + }, + "v2_revision": { "lastModifyingUser" : { "displayName" : "Fitz Elliott", "kind" : "drive#user", @@ -172,102 +148,27 @@ "kind" : "drive#revision", "published" : false, "modifiedDate" : "2017-02-24T18:36:58.041Z", - "id" : "1" + "id" : "1" } }, "viewable_gdoc": { "metadata": { - "createdDate" : "2017-05-15T17:45:45.246Z", - "ownerNames" : [ - "Fitz Elliott" - ], - "appDataContents" : false, - "alternateLink" : "https://docs.google.com/a/cos.io/document/d/1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8/edit?usp=drivesdk", - "mimeType" : "application/vnd.google-apps.document", - "explicitlyTrashed" : false, - "iconLink" : "https://drive-thirdparty.googleusercontent.com/16/type/application/vnd.google-apps.document", - "title" : "viewable_gdoc", - "sharingUser" : { - "isAuthenticatedUser" : false, - "emailAddress" : "fitz.elliott@gmail.com", - "kind" : "drive#user", - "permissionId" : "14130805958117813174", - "displayName" : "Fitz Elliott" - }, - "labels" : { - "trashed" : false, - "hidden" : false, - "viewed" : true, - "restricted" : false, - "starred" : false - }, - "userPermission" : { - "id" : "me", - "role" : "reader", - "kind" : "drive#permission", - "type" : "user", - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/sFn5tJwi_oV7gdwOzjpuBu5Ib28\"", - "selfLink" : "https://www.googleapis.com/drive/v2/files/1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8/permissions/me" - }, - "exportLinks" : { - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=docx", - "text/html" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=html", - "application/pdf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=pdf", - "application/zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=zip", - "application/vnd.oasis.opendocument.text" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=odt", - "application/rtf" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=rtf", - "application/epub+zip" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=epub", - "text/plain" : "https://docs.google.com/feeds/download/documents/export/Export?id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&exportFormat=txt" - }, - "shared" : true, - "lastModifyingUser" : { - "isAuthenticatedUser" : false, - "emailAddress" : "fitz.elliott@gmail.com", - "kind" : "drive#user", - "permissionId" : "14130805958117813174", - "displayName" : "Fitz Elliott" - }, - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/MTQ5NDg3MDM2NTYyOQ\"", - "selfLink" : "https://www.googleapis.com/drive/v2/files/1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8", - "spaces" : [ - "drive" - ], - "parents" : [ - { - "parentLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "kind" : "drive#parentReference", - "id" : "0B74RCNS4TbRVNGJwLWFacnlKdGM", - "isRoot" : false, - "selfLink" : "https://www.googleapis.com/drive/v2/files/1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8/parents/0B74RCNS4TbRVNGJwLWFacnlKdGM" - } - ], - "writersCanShare" : true, - "copyable" : true, - "owners" : [ - { - "emailAddress" : "fitz.elliott@gmail.com", - "kind" : "drive#user", - "isAuthenticatedUser" : false, - "displayName" : "Fitz Elliott", - "permissionId" : "14130805958117813174" - } - ], - "modifiedDate" : "2017-05-15T17:46:05.629Z", - "lastModifyingUserName" : "Fitz Elliott", - "embedLink" : "https://docs.google.com/a/cos.io/document/d/1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8/preview", - "editable" : false, - "id" : "1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8", - "kind" : "drive#file", - "markedViewedByMeDate" : "1970-01-01T00:00:00.000Z", - "capabilities" : { - "canCopy" : true, - "canEdit" : false - }, - "thumbnailLink" : "https://docs.google.com/a/cos.io/feeds/vt?gd=true&id=1k05-O-4a79X4ysubQSxHow6ZhgVVEE1oM-f3hXAWvj8&v=1&s=AMedNnoAAAAAWRo0v0XudaVK3uVkQeUG8-VYyFN49Zne&sz=s220", - "version" : "58109", - "lastViewedByMeDate" : "2017-05-15T17:50:17.249Z", - "sharedWithMeDate" : "2017-05-15T17:46:34.835Z", - "quotaBytesUsed" : "0" + "id": "1B95iwzZnU032YUFvyl8kjVnx-eGlJz6FrIRt-DDxQKw", + "name": "viewable_gdoc", + "mimeType": "application/vnd.google-apps.document", + "webViewLink": "https://docs.google.com/document/d/1B95iwzZnU032YUFvyl8kjVnx-eGlJz6FrIRt-DDxQKw/edit?usp=drivesdk", + "createdTime": "2017-10-13T17:07:04.259Z", + "modifiedTime": "2017-10-13T17:07:13.850Z", + "capabilities": { + "canCopy": false, + "canDelete": false, + "canDownload": true, + "canEdit": false, + "canReadRevisions": false, + "canRename": false, + "canShare": false, + "canTrash": false + } }, "revisions_error": { "error": { @@ -290,200 +191,60 @@ }, "editable_jpeg": { "metadata": { - "shared" : false, - "capabilities" : { - "canCopy" : true, - "canEdit" : true - }, - "alternateLink" : "https://drive.google.com/a/cos.io/file/d/0B74RCNS4TbRVTGs4OTNock9ma0k/view?usp=drivesdk", - "lastModifyingUser" : { - "displayName" : "Fitz Elliott", - "permissionId" : "17667957036718347700", - "isAuthenticatedUser" : true, - "kind" : "drive#user", - "emailAddress" : "fitz@cos.io" - }, - "fileExtension" : "jpeg", - "headRevisionId" : "0B74RCNS4TbRVTitFais4VzVmQlQ4S0docGlhelk5MXE3OFJnPQ", - "explicitlyTrashed" : false, - "fileSize" : "24", - "modifiedByMeDate" : "2017-05-15T20:01:21.364Z", - "iconLink" : "https://drive-thirdparty.googleusercontent.com/16/type/application/octet-stream", - "webContentLink" : "https://drive.google.com/a/cos.io/uc?id=0B74RCNS4TbRVTGs4OTNock9ma0k&export=download", - "userPermission" : { - "type" : "user", - "role" : "owner", - "id" : "me", - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/JhOaef_gJgidSRJMOjo1GIFUDpU\"", - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVTGs4OTNock9ma0k/permissions/me", - "kind" : "drive#permission" - }, - "lastModifyingUserName" : "Fitz Elliott", - "ownerNames" : [ - "Fitz Elliott" - ], - "version" : "48259", - "writersCanShare" : true, - "lastViewedByMeDate" : "2017-05-15T20:00:30.278Z", - "copyable" : true, - "title" : "editable_jpeg.jpeg", - "embedLink" : "https://drive.google.com/a/cos.io/file/d/0B74RCNS4TbRVTGs4OTNock9ma0k/preview?usp=drivesdk", - "owners" : [ - { - "kind" : "drive#user", - "emailAddress" : "fitz@cos.io", - "isAuthenticatedUser" : true, - "permissionId" : "17667957036718347700", - "displayName" : "Fitz Elliott" - } - ], - "markedViewedByMeDate" : "1970-01-01T00:00:00.000Z", - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVTGs4OTNock9ma0k", - "labels" : { - "starred" : false, - "hidden" : false, - "restricted" : false, - "trashed" : false, - "viewed" : true - }, - "id" : "0B74RCNS4TbRVTGs4OTNock9ma0k", - "md5Checksum" : "b7705327bf8ca279454028343d45f3ce", - "parents" : [ - { - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVTGs4OTNock9ma0k/parents/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "parentLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "kind" : "drive#parentReference", - "isRoot" : false, - "id" : "0B74RCNS4TbRVNGJwLWFacnlKdGM" - } - ], - "spaces" : [ - "drive" - ], - "kind" : "drive#file", - "downloadUrl" : "https://doc-0k-bs-docs.googleusercontent.com/docs/securesc/l7i1tlu42rp555c40740almu52k58sf3/8p50odb9i4jucb432pvfm7lfuug7tj21/1494878400000/17667957036718347700/17667957036718347700/0B74RCNS4TbRVTGs4OTNock9ma0k?h=02276134969107310156&e=download&gd=true", - "modifiedDate" : "2017-05-15T20:01:21.364Z", - "quotaBytesUsed" : "24", - "originalFilename" : "editable_jpeg.jpeg", - "editable" : true, - "mimeType" : "application/octet-stream", - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/MTQ5NDg3ODQ4MTM2NA\"", - "createdDate" : "2017-03-13T16:22:39.007Z", - "appDataContents" : false + "id": "0BxdmnuT5XpqcdWJkLU14QXBqdzA", + "name": "editable_jpeg.jpg", + "mimeType": "image/jpeg", + "version": "3", + "webContentLink": "https://drive.google.com/uc?id=0BxdmnuT5XpqcdWJkLU14QXBqdzA&export=download", + "webViewLink": "https://drive.google.com/file/d/0BxdmnuT5XpqcdWJkLU14QXBqdzA/view?usp=drivesdk", + "createdTime": "2017-10-16T13:27:10.965Z", + "modifiedTime": "2017-10-16T13:27:48.203Z", + "capabilities": { + "canCopy": true, + "canDelete": true, + "canDownload": true, + "canEdit": true, + "canReadRevisions": true, + "canRename": true, + "canShare": true, + "canTrash": true + }, + "md5Checksum": "2f7be16736648476f22f08e2e3978d08", + "size": "3782" }, "revision": { - "published" : false, - "etag" : "\"LUxk1DXE_0fd4yeJDIgpecr5uPA/178AcdJuH_nwqx-A-gtOqBTgFk8\"", - "kind" : "drive#revision", - "originalFilename" : "woof", - "lastModifyingUser" : { - "emailAddress" : "fitz@cos.io", - "isAuthenticatedUser" : true, - "displayName" : "Fitz Elliott", - "permissionId" : "17667957036718347700", - "kind" : "drive#user" - }, - "id" : "0B74RCNS4TbRVTitFais4VzVmQlQ4S0docGlhelk5MXE3OFJnPQ", - "fileSize" : "24", - "modifiedDate" : "2017-05-15T20:00:30.278Z", - "mimeType" : "application/octet-stream", - "lastModifyingUserName" : "Fitz Elliott", - "md5Checksum" : "b7705327bf8ca279454028343d45f3ce", - "downloadUrl" : "https://doc-0k-bs-docs.googleusercontent.com/docs/securesc/l7i1tlu42rp555c40740almu52k58sf3/2o2i1vilnakajd9fsh4d3lq8d1ant01g/1495483200000/17667957036718347700/17667957036718347700/0B74RCNS4TbRVTGs4OTNock9ma0k?rid=0B74RCNS4TbRVTitFais4VzVmQlQ4S0docGlhelk5MXE3OFJnPQ&h=02276134969107310156&e=download&gd=true", - "pinned" : true, - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVTGs4OTNock9ma0k/revisions/0B74RCNS4TbRVTitFais4VzVmQlQ4S0docGlhelk5MXE3OFJnPQ" + "id": "0BxdmnuT5XpqcdFdzM2dtTnB4bnNuUXpmYlZkOUZDV1AvcWFRPQ", + "mimeType": "image/jpeg", + "modifiedTime": "2017-10-16T13:27:10.965Z", + "originalFilename": "sunshine.jpg", + "md5Checksum": "2f7be16736648476f22f08e2e3978d08", + "size": "3782" } }, "viewable_jpeg": { "metadata": { - "lastModifyingUser" : { - "isAuthenticatedUser" : false, - "displayName" : "Fitz Elliott", - "emailAddress" : "fitz.elliott@gmail.com", - "permissionId" : "14130805958117813174", - "kind" : "drive#user" - }, - "labels" : { - "viewed" : true, - "hidden" : false, - "starred" : false, - "trashed" : false, - "restricted" : false - }, - "alternateLink" : "https://drive.google.com/a/cos.io/file/d/0B5RhExds5_WHYnp6Um9fOGpwVVU/view?usp=drivesdk", - "version" : "58119", - "mimeType" : "text/plain", - "thumbnailLink" : "https://lh5.googleusercontent.com/w8fu-Im3cwup-k_B3lz5yVsxM6SSISeiVR0h6vochLOQiFKuixc_vvWRr46iCTkHXb-Bpg=s220", - "kind" : "drive#file", - "owners" : [ - { - "kind" : "drive#user", - "permissionId" : "14130805958117813174", - "emailAddress" : "fitz.elliott@gmail.com", - "displayName" : "Fitz Elliott", - "isAuthenticatedUser" : false - } - ], - "writersCanShare" : true, - "webContentLink" : "https://drive.google.com/a/cos.io/uc?id=0B5RhExds5_WHYnp6Um9fOGpwVVU&export=download", - "copyable" : true, - "spaces" : [ - "drive" - ], - "sharedWithMeDate" : "2017-05-15T17:50:43.573Z", - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/MTQ5NDg3MDY0MzU3MQ\"", - "lastModifyingUserName" : "Fitz Elliott", - "createdDate" : "2017-05-15T17:50:23.253Z", - "headRevisionId" : "0B5RhExds5_WHdkJWc0dZS2pZVk1SOTd1bVlydktHbVBJSEd3PQ", - "iconLink" : "https://drive-thirdparty.googleusercontent.com/16/type/text/plain", - "lastViewedByMeDate" : "2017-05-15T17:50:53.135Z", - "markedViewedByMeDate" : "1970-01-01T00:00:00.000Z", - "id" : "0B5RhExds5_WHYnp6Um9fOGpwVVU", - "explicitlyTrashed" : false, - "downloadUrl" : "https://doc-00-78-docs.googleusercontent.com/docs/securesc/l7i1tlu42rp555c40740almu52k58sf3/sso0ksodpgkj6uka1v2of0ms11dtguv7/1494878400000/14130805958117813174/17667957036718347700/0B5RhExds5_WHYnp6Um9fOGpwVVU?h=02276134969107310156&e=download&gd=true", - "capabilities" : { - "canCopy" : true, - "canEdit" : false - }, - "md5Checksum" : "8219a2180f96d8e85092a4fea17445d4", - "appDataContents" : false, - "quotaBytesUsed" : "0", - "fileSize" : "30", - "modifiedDate" : "2017-05-15T17:50:43.571Z", - "originalFilename" : "viewable_jpeg.jpeg", - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B5RhExds5_WHYnp6Um9fOGpwVVU", - "userPermission" : { - "type" : "user", - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B5RhExds5_WHYnp6Um9fOGpwVVU/permissions/me", - "role" : "reader", - "kind" : "drive#permission", - "id" : "me", - "etag" : "\"_Iwf20PbJS7QPBy9REF76uRLkQo/eimdML0wdpTxibfYHooVAmGWumk\"" - }, - "editable" : false, - "ownerNames" : [ - "Fitz Elliott" - ], - "fileExtension" : "jpeg", - "shared" : true, - "sharingUser" : { - "permissionId" : "14130805958117813174", - "emailAddress" : "fitz.elliott@gmail.com", - "kind" : "drive#user", - "displayName" : "Fitz Elliott", - "isAuthenticatedUser" : false - }, - "parents" : [ - { - "selfLink" : "https://www.googleapis.com/drive/v2/files/0B5RhExds5_WHYnp6Um9fOGpwVVU/parents/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "isRoot" : false, - "kind" : "drive#parentReference", - "parentLink" : "https://www.googleapis.com/drive/v2/files/0B74RCNS4TbRVNGJwLWFacnlKdGM", - "id" : "0B74RCNS4TbRVNGJwLWFacnlKdGM" - } - ], - "embedLink" : "https://drive.google.com/a/cos.io/file/d/0B5RhExds5_WHYnp6Um9fOGpwVVU/preview?usp=drivesdk", - "title" : "viewable_jpeg.jpeg" + "id": "0BxdmnuT5XpqcdWJkLU14QXBqdzA", + "name": "editable_jpeg.jpg", + "mimeType": "image/jpeg", + "version": "3", + "webContentLink": "https://drive.google.com/uc?id=0BxdmnuT5XpqcdWJkLU14QXBqdzA&export=download", + "webViewLink": "https://drive.google.com/file/d/0BxdmnuT5XpqcdWJkLU14QXBqdzA/view?usp=drivesdk", + "createdTime": "2017-10-16T13:27:10.965Z", + "modifiedTime": "2017-10-16T13:27:48.203Z", + "capabilities": { + "canCopy": false, + "canDelete": false, + "canDownload": true, + "canEdit": false, + "canReadRevisions": false, + "canRename": false, + "canShare": false, + "canTrash": false + }, + "md5Checksum": "2f7be16736648476f22f08e2e3978d08", + "size": "3782" + }, + "revision": { } }, "commentable_jpeg": { diff --git a/tests/providers/googledrive/test_metadata.py b/tests/providers/googledrive/test_metadata.py index f425504ea..2ede6f46e 100644 --- a/tests/providers/googledrive/test_metadata.py +++ b/tests/providers/googledrive/test_metadata.py @@ -24,92 +24,92 @@ def basepath(): class TestMetadata: def test_file_metadata_drive(self, basepath, root_provider_fixtures): - item = root_provider_fixtures['list_file']['items'][0] - path = basepath.child(item['title']) - parsed = GoogleDriveFileMetadata(item, path) + file = root_provider_fixtures['list_file']['files'][0] + path = basepath.child(file['name']) + parsed = GoogleDriveFileMetadata(file, path) assert parsed.provider == 'googledrive' - assert parsed.id == item['id'] - assert path.name == item['title'] - assert parsed.name == item['title'] - assert parsed.size == item['fileSize'] - assert parsed.modified == item['modifiedDate'] - assert parsed.content_type == item['mimeType'] + assert parsed.id == file['id'] + assert path.name == file['name'] + assert parsed.name == file['name'] + assert parsed.size == file['size'] + assert parsed.modified == file['modifiedTime'] + assert parsed.content_type == file['mimeType'] assert parsed.extra == { - 'revisionId': item['version'], - 'webView': item['alternateLink'], - 'hashes': {'md5': item['md5Checksum']}, + 'revisionId': file['version'], + 'webView': file['webViewLink'], + 'hashes': {'md5': file['md5Checksum']}, } assert parsed.path == '/' + os.path.join(*[x.raw for x in path.parts]) assert parsed.materialized_path == str(path) assert parsed.is_google_doc is False - assert parsed.export_name == item['title'] + assert parsed.export_name == file['name'] def test_file_metadata_drive_slashes(self, basepath, root_provider_fixtures): - item = root_provider_fixtures['file_forward_slash'] - path = basepath.child(item['title']) - parsed = GoogleDriveFileMetadata(item, path) + file = root_provider_fixtures['file_forward_slash'] + path = basepath.child(file['name']) + parsed = GoogleDriveFileMetadata(file, path) assert parsed.provider == 'googledrive' - assert parsed.id == item['id'] - assert parsed.name == item['title'] + assert parsed.id == file['id'] + assert parsed.name == file['name'] assert parsed.name == path.name - assert parsed.size == item['fileSize'] - assert parsed.modified == item['modifiedDate'] - assert parsed.content_type == item['mimeType'] + assert parsed.size == file['size'] + assert parsed.modified == file['modifiedTime'] + assert parsed.content_type == file['mimeType'] assert parsed.extra == { - 'revisionId': item['version'], - 'webView': item['alternateLink'], - 'hashes': {'md5': item['md5Checksum']}, + 'revisionId': file['version'], + 'webView': file['webViewLink'], + 'hashes': {'md5': file['md5Checksum']}, } assert parsed.path == '/' + os.path.join(*[x.raw for x in path.parts]) assert parsed.materialized_path == str(path) assert parsed.is_google_doc is False - assert parsed.export_name == item['title'] + assert parsed.export_name == file['name'] def test_file_metadata_docs(self, basepath, root_provider_fixtures): - item = root_provider_fixtures['docs_file_metadata'] - path = basepath.child(item['title']) - parsed = GoogleDriveFileMetadata(item, path) + file = root_provider_fixtures['docs_file_metadata'] + path = basepath.child(file['name']) + parsed = GoogleDriveFileMetadata(file, path) - assert parsed.name == item['title'] + '.gdoc' + assert parsed.name == file['name'] + '.gdoc' assert parsed.extra == { - 'revisionId': item['version'], + 'revisionId': file['version'], 'downloadExt': '.docx', - 'webView': item['alternateLink'], + 'webView': file['webViewLink'], } assert parsed.is_google_doc is True - assert parsed.export_name == item['title'] + '.docx' + assert parsed.export_name == file['name'] + '.docx' def test_folder_metadata(self, root_provider_fixtures): - item = root_provider_fixtures['folder_metadata'] - path = GoogleDrivePath('/we/love/you/conrad').child(item['title'], folder=True) - parsed = GoogleDriveFolderMetadata(item, path) + file = root_provider_fixtures['folder_metadata'] + path = GoogleDrivePath('/we/love/you/conrad').child(file['name'], folder=True) + parsed = GoogleDriveFolderMetadata(file, path) assert parsed.provider == 'googledrive' - assert parsed.id == item['id'] - assert parsed.name == item['title'] - assert parsed.extra == {'revisionId': item['version']} + assert parsed.id == file['id'] + assert parsed.name == file['name'] + assert parsed.extra == {'revisionId': file['version']} assert parsed.path == '/' + os.path.join(*[x.raw for x in path.parts]) + '/' assert parsed.materialized_path == str(path) - assert parsed.export_name == item['title'] + assert parsed.export_name == file['name'] def test_folder_metadata_slash(self, root_provider_fixtures): - item = root_provider_fixtures['folder_metadata_forward_slash'] - path = GoogleDrivePath('/we/love/you/conrad').child(item['title'], folder=True) - parsed = GoogleDriveFolderMetadata(item, path) + file = root_provider_fixtures['folder_metadata_forward_slash'] + path = GoogleDrivePath('/we/love/you/conrad').child(file['name'], folder=True) + parsed = GoogleDriveFolderMetadata(file, path) assert parsed.provider == 'googledrive' - assert parsed.id == item['id'] - assert parsed.name == item['title'] - assert parsed.extra == {'revisionId': item['version']} + assert parsed.id == file['id'] + assert parsed.name == file['name'] + assert parsed.extra == {'revisionId': file['version']} assert parsed.path == '/' + os.path.join(*[x.raw for x in path.parts]) + '/' assert parsed.materialized_path == str(path) - assert parsed.export_name == item['title'] + assert parsed.export_name == file['name'] def test_revision_metadata(self, revision_fixtures): - item = revision_fixtures['revision_metadata'] - parsed = GoogleDriveRevision(item) + revision = revision_fixtures['revision_metadata'] + parsed = GoogleDriveRevision(revision) assert parsed.version_identifier == 'revision' - assert parsed.version == item['id'] - assert parsed.modified == item['modifiedDate'] + assert parsed.version == revision['id'] + assert parsed.modified == revision['modifiedTime'] diff --git a/tests/providers/googledrive/test_provider.py b/tests/providers/googledrive/test_provider.py index 81624f5a5..4d058ce8d 100644 --- a/tests/providers/googledrive/test_provider.py +++ b/tests/providers/googledrive/test_provider.py @@ -5,6 +5,7 @@ from http import client from urllib import parse +import furl import pytest import aiohttpretty @@ -83,7 +84,7 @@ def other_provider(auth, other_credentials, settings): @pytest.fixture def search_for_file_response(): return { - 'items': [ + 'files': [ {'id': '1234ideclarethumbwar'} ] } @@ -92,7 +93,7 @@ def search_for_file_response(): @pytest.fixture def no_file_response(): return { - 'items': [] + 'files': [] } @@ -101,14 +102,14 @@ def actual_file_response(): return { 'id': '1234ideclarethumbwar', 'mimeType': 'text/plain', - 'title': 'B.txt', + 'name': 'B.txt', } @pytest.fixture def search_for_folder_response(): return { - 'items': [ + 'files': [ {'id': 'whyis6afraidof7'} ] } @@ -117,7 +118,7 @@ def search_for_folder_response(): @pytest.fixture def no_folder_response(): return { - 'items': [] + 'files': [] } @@ -126,7 +127,7 @@ def actual_folder_response(): return { 'id': 'whyis6afraidof7', 'mimeType': 'application/vnd.google-apps.folder', - 'title': 'A', + 'name': 'A', } @@ -159,7 +160,8 @@ def make_no_such_revision_error(revision_id): "reason": "notFound", "locationType": "other", "message": message, - "location": "revision", + "locationType": "parameter", + "location": "revisionId", "domain": "global" } ], @@ -175,8 +177,8 @@ def clean_query(query: str): return query.replace('\\', r'\\').replace("'", r"\'") -def _build_title_search_query(provider, entity_name, is_folder=True): - return "title = '{}' " \ +def _build_name_search_query(provider, entity_name, base_id, is_folder=True): + return "name = '{}' " \ "and trashed = false " \ "and mimeType != 'application/vnd.google-apps.form' " \ "and mimeType != 'application/vnd.google-apps.map' " \ @@ -184,19 +186,21 @@ def _build_title_search_query(provider, entity_name, is_folder=True): "and mimeType != 'application/vnd.google-apps.drawing' " \ "and mimeType != 'application/vnd.google-apps.presentation' " \ "and mimeType != 'application/vnd.google-apps.spreadsheet' " \ + "and '{}' in parents " \ "and mimeType {} '{}'".format( entity_name, + base_id, '=' if is_folder else '!=', provider.FOLDER_MIME_TYPE ) def generate_list(child_id, **kwargs): - item = {} - item.update(root_provider_fixtures()['list_file']['items'][0]) - item.update(kwargs) - item['id'] = str(child_id) - return {'items': [item]} + file = {} + file.update(root_provider_fixtures()['list_file']['files'][0]) + file.update(kwargs) + file['id'] = str(child_id) + return {'files': [file]} class TestValidatePath: @@ -209,16 +213,16 @@ async def test_validate_v1_path_file(self, provider, search_for_file_response, file_id = '1234ideclarethumbwar' query_url = provider.build_url( - 'files', provider.folder['id'], 'children', - q=_build_title_search_query(provider, file_name, False), - fields='items(id)' + 'files', + q=_build_name_search_query(provider, file_name, provider.folder['id'], False), + fields='files(id)' ) wrong_query_url = provider.build_url( - 'files', provider.folder['id'], 'children', - q=_build_title_search_query(provider, file_name, True), - fields='items(id)' + 'files', + q=_build_name_search_query(provider, file_name, provider.folder['id'], True), + fields='files(id)' ) - specific_url = provider.build_url('files', file_id, fields='id,title,mimeType') + specific_url = provider.build_url('files', file_id, fields='id, name, mimeType') aiohttpretty.register_json_uri('GET', query_url, body=search_for_file_response) aiohttpretty.register_json_uri('GET', wrong_query_url, body=no_folder_response) @@ -246,16 +250,16 @@ async def test_validate_v1_path_folder(self, provider, search_for_folder_respons folder_id = 'whyis6afraidof7' query_url = provider.build_url( - 'files', provider.folder['id'], 'children', - q=_build_title_search_query(provider, folder_name, True), - fields='items(id)' + 'files', + q=_build_name_search_query(provider, folder_name, provider.folder['id'], True), + fields='files(id)' ) wrong_query_url = provider.build_url( - 'files', provider.folder['id'], 'children', - q=_build_title_search_query(provider, folder_name, False), - fields='items(id)' + 'files', + q=_build_name_search_query(provider, folder_name, provider.folder['id'], False), + fields='files(id)' ) - specific_url = provider.build_url('files', folder_id, fields='id,title,mimeType') + specific_url = provider.build_url('files', folder_id, fields='id, name, mimeType') aiohttpretty.register_json_uri('GET', query_url, body=search_for_folder_response) aiohttpretty.register_json_uri('GET', wrong_query_url, body=no_file_response) @@ -290,7 +294,7 @@ async def test_validate_v1_path_root(self, provider): async def test_revalidate_path_file(self, provider, root_provider_fixtures): file_name = '/Gear1.stl' revalidate_path_metadata = root_provider_fixtures['revalidate_path_file_metadata_1'] - file_id = revalidate_path_metadata['items'][0]['id'] + file_id = revalidate_path_metadata['files'][0]['id'] path = GoogleDrivePath(file_name, _ids=['0', file_id]) parts = [[parse.unquote(x), True] for x in file_name.strip('/').split('/')] @@ -299,12 +303,12 @@ async def test_revalidate_path_file(self, provider, root_provider_fixtures): current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] name, ext = os.path.splitext(part_name) - query = _build_title_search_query(provider, file_name.strip('/'), False) + query = _build_name_search_query(provider, file_name.strip('/'), file_id, False) - url = provider.build_url('files', file_id, 'children', q=query, fields='items(id)') + url = provider.build_url('files', q=query, fields='files(id)') aiohttpretty.register_json_uri('GET', url, body=revalidate_path_metadata) - url = provider.build_url('files', file_id, fields='id,title,mimeType') + url = provider.build_url('files', file_id, fields='id, name, mimeType') aiohttpretty.register_json_uri('GET', url, body=root_provider_fixtures['revalidate_path_file_metadata_2']) @@ -316,7 +320,7 @@ async def test_revalidate_path_file(self, provider, root_provider_fixtures): @pytest.mark.aiohttpretty async def test_revalidate_path_file_gdoc(self, provider, root_provider_fixtures): file_name = '/Gear1.gdoc' - file_id = root_provider_fixtures['revalidate_path_file_metadata_1']['items'][0]['id'] + file_id = root_provider_fixtures['revalidate_path_file_metadata_1']['files'][0]['id'] path = GoogleDrivePath(file_name, _ids=['0', file_id]) parts = [[parse.unquote(x), True] for x in file_name.strip('/').split('/')] @@ -326,15 +330,16 @@ async def test_revalidate_path_file_gdoc(self, provider, root_provider_fixtures) part_name, part_is_folder = current_part[0], current_part[1] name, ext = os.path.splitext(part_name) gd_ext = drive_utils.get_mimetype_from_ext(ext) - query = "title = '{}' " \ + query = "name = '{}' " \ "and trashed = false " \ - "and mimeType = '{}'".format(clean_query(name), gd_ext) + "and '{}' in parents " \ + "and mimeType = '{}'".format(clean_query(name), file_id, gd_ext) - url = provider.build_url('files', file_id, 'children', q=query, fields='items(id)') + url = provider.build_url('files', q=query, fields='files(id)') aiohttpretty.register_json_uri('GET', url, body=root_provider_fixtures['revalidate_path_file_metadata_1']) - url = provider.build_url('files', file_id, fields='id,title,mimeType') + url = provider.build_url('files', file_id, fields='id, name, mimeType') aiohttpretty.register_json_uri('GET', url, body=root_provider_fixtures['revalidate_path_gdoc_file_metadata']) @@ -346,7 +351,7 @@ async def test_revalidate_path_file_gdoc(self, provider, root_provider_fixtures) @pytest.mark.aiohttpretty async def test_revalidate_path_folder(self, provider, root_provider_fixtures): file_name = "/inception folder yo/" - file_id = root_provider_fixtures['revalidate_path_folder_metadata_1']['items'][0]['id'] + file_id = root_provider_fixtures['revalidate_path_folder_metadata_1']['files'][0]['id'] path = GoogleDrivePath(file_name, _ids=['0', file_id]) parts = [[parse.unquote(x), True] for x in file_name.strip('/').split('/')] @@ -355,13 +360,13 @@ async def test_revalidate_path_folder(self, provider, root_provider_fixtures): current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] name, ext = os.path.splitext(part_name) - query = _build_title_search_query(provider, file_name.strip('/') + '/', True) + query = _build_name_search_query(provider, file_name.strip('/') + '/', file_id, True) - folder_one_url = provider.build_url('files', file_id, 'children', q=query, fields='items(id)') + folder_one_url = provider.build_url('files', q=query, fields='files(id)') aiohttpretty.register_json_uri('GET', folder_one_url, body=root_provider_fixtures['revalidate_path_folder_metadata_1']) - folder_two_url = provider.build_url('files', file_id, fields='id,title,mimeType') + folder_two_url = provider.build_url('files', file_id, fields='id, name, mimeType') aiohttpretty.register_json_uri('GET', folder_two_url, body=root_provider_fixtures['revalidate_path_folder_metadata_2']) @@ -375,44 +380,48 @@ class TestUpload: @pytest.mark.aiohttpretty async def test_upload_create(self, provider, file_stream, root_provider_fixtures): upload_id = '7' - item = root_provider_fixtures['list_file']['items'][0] + file = root_provider_fixtures['list_file']['files'][0] path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], None)) - start_upload_url = provider._build_upload_url('files', uploadType='resumable') + start_upload_furl = furl.furl(provider._build_upload_url('files', '', + uploadType='resumable')) + start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url finish_upload_url = provider._build_upload_url('files', uploadType='resumable', upload_id=upload_id) - aiohttpretty.register_json_uri('PUT', finish_upload_url, body=item) aiohttpretty.register_uri('POST', start_upload_url, headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) result, created = await provider.upload(file_stream, path) - expected = GoogleDriveFileMetadata(item, path) + expected = GoogleDriveFileMetadata(file, path) assert created is True assert result == expected - assert aiohttpretty.has_call(method='PUT', uri=finish_upload_url) assert aiohttpretty.has_call(method='POST', uri=start_upload_url) + assert aiohttpretty.has_call(method='PUT', uri=finish_upload_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_upload_doesnt_unquote(self, provider, file_stream, root_provider_fixtures): upload_id = '7' - item = root_provider_fixtures['list_file']['items'][0] + file = root_provider_fixtures['list_file']['files'][0] path = GoogleDrivePath('/birdie%2F %20".jpg', _ids=(provider.folder['id'], None)) - start_upload_url = provider._build_upload_url('files', uploadType='resumable') + start_upload_furl = furl.furl(provider._build_upload_url('files', '', + uploadType='resumable')) + start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url finish_upload_url = provider._build_upload_url('files', uploadType='resumable', upload_id=upload_id) - aiohttpretty.register_json_uri('PUT', finish_upload_url, body=item) aiohttpretty.register_uri('POST', start_upload_url, headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) result, created = await provider.upload(file_stream, path) - expected = GoogleDriveFileMetadata(item, path) + expected = GoogleDriveFileMetadata(file, path) assert created is True assert result == expected @@ -423,47 +432,50 @@ async def test_upload_doesnt_unquote(self, provider, file_stream, root_provider_ @pytest.mark.aiohttpretty async def test_upload_update(self, provider, file_stream, root_provider_fixtures): upload_id = '7' - item = root_provider_fixtures['list_file']['items'][0] - path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], item['id'])) + file = root_provider_fixtures['list_file']['files'][0] + path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], file['id'])) - start_upload_url = provider._build_upload_url('files', path.identifier, - uploadType='resumable') + start_upload_furl = furl.furl(provider._build_upload_url('files', file['id'], + uploadType='resumable')) + start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url finish_upload_url = provider._build_upload_url('files', path.identifier, uploadType='resumable', upload_id=upload_id) - aiohttpretty.register_json_uri('PUT', finish_upload_url, body=item) - aiohttpretty.register_uri('PUT', start_upload_url, + aiohttpretty.register_uri('PATCH', start_upload_url, headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) result, created = await provider.upload(file_stream, path) - assert aiohttpretty.has_call(method='PUT', uri=start_upload_url) + assert aiohttpretty.has_call(method='PATCH', uri=start_upload_url) assert aiohttpretty.has_call(method='PUT', uri=finish_upload_url) assert created is False - expected = GoogleDriveFileMetadata(item, path) + expected = GoogleDriveFileMetadata(file, path) assert result == expected @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_upload_create_nested(self, provider, file_stream, root_provider_fixtures): upload_id = '7' - item = root_provider_fixtures['list_file']['items'][0] + file = root_provider_fixtures['list_file']['files'][0] path = WaterButlerPath( '/ed/sullivan/show.mp3', _ids=[str(x) for x in range(3)] ) - start_upload_url = provider._build_upload_url('files', uploadType='resumable') + start_upload_furl = furl.furl(provider._build_upload_url('files', '', + uploadType='resumable')) + start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url finish_upload_url = provider._build_upload_url('files', uploadType='resumable', upload_id=upload_id) aiohttpretty.register_uri('POST', start_upload_url, headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) - aiohttpretty.register_json_uri('PUT', finish_upload_url, body=item) + aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) result, created = await provider.upload(file_stream, path) assert aiohttpretty.has_call(method='POST', uri=start_upload_url) assert aiohttpretty.has_call(method='PUT', uri=finish_upload_url) assert created is True - expected = GoogleDriveFileMetadata(item, path) + expected = GoogleDriveFileMetadata(file, path) assert result == expected @pytest.mark.asyncio @@ -472,7 +484,9 @@ async def test_upload_checksum_mismatch(self, provider, file_stream, root_provid upload_id = '7' path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], None)) - start_upload_url = provider._build_upload_url('files', uploadType='resumable') + start_upload_furl = furl.furl(provider._build_upload_url('files', '', + uploadType='resumable')) + start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url finish_upload_url = provider._build_upload_url('files', uploadType='resumable', upload_id=upload_id) @@ -493,11 +507,11 @@ class TestDelete: @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_delete(self, provider, root_provider_fixtures): - item = root_provider_fixtures['list_file']['items'][0] - path = WaterButlerPath('/birdie.jpg', _ids=(None, item['id'])) - delete_url = provider.build_url('files', item['id']) + file = root_provider_fixtures['list_file']['files'][0] + path = WaterButlerPath('/birdie.jpg', _ids=(None, file['id'])) + delete_url = provider.build_url('files', file['id']) del_url_body = json.dumps({'labels': {'trashed': 'true'}}) - aiohttpretty.register_uri('PUT', + aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) @@ -505,7 +519,7 @@ async def test_delete(self, provider, root_provider_fixtures): result = await provider.delete(path) assert result is None - assert aiohttpretty.has_call(method='PUT', uri=delete_url) + assert aiohttpretty.has_call(method='PATCH', uri=delete_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -516,14 +530,14 @@ async def test_delete_folder(self, provider, root_provider_fixtures): path = WaterButlerPath('/foobar/', _ids=('doesntmatter', item['id'])) - aiohttpretty.register_uri('PUT', + aiohttpretty.register_uri('PATCH', del_url, body=del_url_body, status=200) result = await provider.delete(path) - assert aiohttpretty.has_call(method='PUT', uri=del_url) + assert aiohttpretty.has_call(method='PATCH', uri=del_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -545,20 +559,21 @@ async def test_delete_root_no_confirm(self, provider): @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_delete_root(self, provider, root_provider_fixtures): - item = root_provider_fixtures['delete_contents_metadata']['items'][0] + file = root_provider_fixtures['delete_contents_metadata']['files'][0] root_path = WaterButlerPath('/', _ids=('0')) - url = provider.build_url('files', q="'{}' in parents".format('0'), fields='items(id)') + url = provider.build_url('files', q="'{}' in parents and trashed = false".format('0'), + fields='files(id)') aiohttpretty.register_json_uri('GET', url, body=root_provider_fixtures['delete_contents_metadata']) - delete_url = provider.build_url('files', item['id']) + delete_url = provider.build_url('files', file['id']) data = json.dumps({'labels': {'trashed': 'true'}}), - aiohttpretty.register_json_uri('PUT', delete_url, data=data, status=200) + aiohttpretty.register_json_uri('PATCH', delete_url, data=data, status=200) await provider.delete(root_path, 1) - assert aiohttpretty.has_call(method='PUT', uri=delete_url) + assert aiohttpretty.has_call(method='PATCH', uri=delete_url) class TestDownload: @@ -581,7 +596,7 @@ class TestDownload: """ GDOC_GOOD_REVISION = '1' - GDOC_BAD_REVISION = '0B74RCNS4TbRVTitFais4VzVmQlQ4S0docGlhelk5MXE3OFJnPQ' + GDOC_BAD_REVISION = '0BxdmnuT5XpqcdFdzM2dtTnB4bnNuUXpmYlZkOUZDV1AvcWFRPQ' JPEG_GOOD_REVISION = GDOC_BAD_REVISION JPEG_BAD_REVISION = GDOC_GOOD_REVISION MAGIC_REVISION = '"LUxk1DXE_0fd4yeJDIgpecr5uPA/MTQ5NTExOTgxMzgzOQ"{}'.format( @@ -598,8 +613,8 @@ async def test_download_editable_gdoc_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) revisions_body = sharing_fixtures['editable_gdoc']['revisions'] @@ -607,7 +622,8 @@ async def test_download_editable_gdoc_no_revision(self, provider, sharing_fixtur aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) file_content = b'we love you conrad' - download_file_url = metadata_body['exportLinks'][self.GDOC_EXPORT_MIME_TYPE] + download_file_url = provider.build_url('files', metadata_body['id'], 'export', + mimeType=self.GDOC_EXPORT_MIME_TYPE) aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path) @@ -628,9 +644,18 @@ async def test_download_editable_gdoc_good_revision(self, provider, sharing_fixt _ids=['1', '2', metadata_body['id']] ) - revision_body = sharing_fixtures['editable_gdoc']['revision'] + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) + + revisions_body = sharing_fixtures['editable_gdoc']['revisions'] + revisions_url = provider.build_url('files', metadata_body['id'], 'revisions') + aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) + + revision_body = sharing_fixtures['editable_gdoc']['v2_revision'] revision_url = provider.build_url('files', metadata_body['id'], 'revisions', self.GDOC_GOOD_REVISION) + revision_url = revision_url.replace('/v3/', '/v2/', 1) aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) file_content = b'we love you conrad' @@ -642,6 +667,8 @@ async def test_download_editable_gdoc_good_revision(self, provider, sharing_fixt content = await result.read() assert content == file_content + assert aiohttpretty.has_call(method='GET', uri=metadata_url) + assert aiohttpretty.has_call(method='GET', uri=revisions_url) assert aiohttpretty.has_call(method='GET', uri=revision_url) assert aiohttpretty.has_call(method='GET', uri=download_file_url) @@ -654,15 +681,27 @@ async def test_download_editable_gdoc_bad_revision(self, provider, sharing_fixtu _ids=['1', '2', metadata_body['id']] ) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) + + revisions_body = sharing_fixtures['editable_gdoc']['revisions'] + revisions_url = provider.build_url('files', metadata_body['id'], 'revisions') + aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) + no_such_revision_error = make_no_such_revision_error(self.GDOC_BAD_REVISION) revision_url = provider.build_url('files', metadata_body['id'], 'revisions', self.GDOC_BAD_REVISION) + revision_url = revision_url.replace('/v3/', '/v2/', 1) aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) with pytest.raises(exceptions.NotFoundError) as e: await provider.download(path, revision=self.GDOC_BAD_REVISION) assert e.value.code == 404 + assert aiohttpretty.has_call(method='GET', uri=metadata_url) + assert aiohttpretty.has_call(method='GET', uri=revisions_url) + assert aiohttpretty.has_call(method='GET', uri=revision_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -673,8 +712,8 @@ async def test_download_editable_gdoc_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) revisions_body = sharing_fixtures['editable_gdoc']['revisions'] @@ -682,8 +721,10 @@ async def test_download_editable_gdoc_magic_revision(self, provider, sharing_fix aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) file_content = b'we love you conrad' - download_file_url = metadata_body['exportLinks'][self.GDOC_EXPORT_MIME_TYPE] - aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) + mime_type = self.GDOC_EXPORT_MIME_TYPE + download_url = provider.build_url('files', path.identifier, 'export', + mimeType=mime_type) + aiohttpretty.register_uri('GET', download_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.MAGIC_REVISION) assert result.name == 'editable_gdoc.docx' @@ -692,23 +733,24 @@ async def test_download_editable_gdoc_magic_revision(self, provider, sharing_fix assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) assert aiohttpretty.has_call(method='GET', uri=revisions_url) - assert aiohttpretty.has_call(method='GET', uri=download_file_url) + assert aiohttpretty.has_call(method='GET', uri=download_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_download_viewable_gdoc_no_revision(self, provider, sharing_fixtures): metadata_body = sharing_fixtures['viewable_gdoc']['metadata'] path = GoogleDrivePath( - '/sharing/viewaable_gdoc', + '/sharing/viewable_gdoc', _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = metadata_body['exportLinks'][self.GDOC_EXPORT_MIME_TYPE] + download_file_url = provider.build_url('files', metadata_body.get('id'), 'export', + mimeType=self.GDOC_EXPORT_MIME_TYPE) aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path) @@ -728,9 +770,18 @@ async def test_download_viewable_gdoc_bad_revision(self, provider, sharing_fixtu _ids=['1', '2', metadata_body['id']] ) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) + + revisions_body = sharing_fixtures['editable_gdoc']['revisions'] + revisions_url = provider.build_url('files', metadata_body['id'], 'revisions') + aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) + unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) revision_url = provider.build_url('files', metadata_body['id'], 'revisions', self.GDOC_BAD_REVISION) + revision_url = revision_url.replace('/v3/', '/v2/', 1) aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) with pytest.raises(exceptions.NotFoundError) as e: @@ -747,13 +798,15 @@ async def test_download_viewable_gdoc_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = metadata_body['exportLinks'][self.GDOC_EXPORT_MIME_TYPE] - aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) + mime_type = self.GDOC_EXPORT_MIME_TYPE + download_url = provider.build_url('files', metadata_body.get('id'), 'export', + mimeType=mime_type) + aiohttpretty.register_uri('GET', download_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.MAGIC_REVISION) assert result.name == 'viewable_gdoc.docx' @@ -761,7 +814,7 @@ async def test_download_viewable_gdoc_magic_revision(self, provider, sharing_fix content = await result.read() assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) - assert aiohttpretty.has_call(method='GET', uri=download_file_url) + assert aiohttpretty.has_call(method='GET', uri=download_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -772,12 +825,12 @@ async def test_download_editable_jpeg_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = metadata_body['downloadUrl'] + download_file_url = metadata_body['webContentLink'] aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path) @@ -796,21 +849,21 @@ async def test_download_editable_jpeg_good_revision(self, provider, sharing_fixt _ids=['1', '2', metadata_body['id']] ) - revision_body = sharing_fixtures['editable_jpeg']['revision'] - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.JPEG_GOOD_REVISION) - aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = revision_body['downloadUrl'] - aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) + download_url = provider.build_url('files', metadata_body.get('id'), + 'revisions', self.JPEG_GOOD_REVISION, alt='media') + aiohttpretty.register_uri('GET', download_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.JPEG_GOOD_REVISION) content = await result.read() assert content == file_content - assert aiohttpretty.has_call(method='GET', uri=revision_url) - assert aiohttpretty.has_call(method='GET', uri=download_file_url) + assert aiohttpretty.has_call(method='GET', uri=metadata_url) + assert aiohttpretty.has_call(method='GET', uri=download_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -821,15 +874,22 @@ async def test_download_editable_jpeg_bad_revision(self, provider, sharing_fixtu _ids=['1', '2', metadata_body['id']] ) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) + no_such_revision_error = make_no_such_revision_error(self.JPEG_BAD_REVISION) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.JPEG_BAD_REVISION) - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) + download_url = provider.build_url('files', metadata_body.get('id'), + 'revisions', self.JPEG_BAD_REVISION, alt='media') + aiohttpretty.register_uri('GET', download_url, status=404, body=no_such_revision_error, + auto_length=True) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.DownloadError) as e: await provider.download(path, revision=self.JPEG_BAD_REVISION) assert e.value.code == 404 + assert aiohttpretty.has_call(method='GET', uri=metadata_url) + assert aiohttpretty.has_call(method='GET', uri=download_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -840,12 +900,12 @@ async def test_download_editable_jpeg_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = metadata_body['downloadUrl'] + download_file_url = metadata_body['webContentLink'] aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.MAGIC_REVISION) @@ -864,12 +924,12 @@ async def test_download_viewable_jpeg_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = metadata_body['downloadUrl'] + download_file_url = metadata_body['webContentLink'] aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path) @@ -888,15 +948,22 @@ async def test_download_viewable_jpeg_bad_revision(self, provider, sharing_fixtu _ids=['1', '2', metadata_body['id']] ) - unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.JPEG_BAD_REVISION) - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) - with pytest.raises(exceptions.NotFoundError) as e: + no_such_revision_error = make_no_such_revision_error(metadata_body['id']) + download_url = provider.build_url('files', metadata_body.get('id'), + 'revisions', self.JPEG_BAD_REVISION, alt='media') + aiohttpretty.register_uri('GET', download_url, status=404, body=no_such_revision_error, + auto_length=True) + + with pytest.raises(exceptions.DownloadError) as e: await provider.download(path, revision=self.JPEG_BAD_REVISION) assert e.value.code == 404 + assert aiohttpretty.has_call(method='GET', uri=metadata_url) + assert aiohttpretty.has_call(method='GET', uri=download_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -907,12 +974,12 @@ async def test_download_viewable_jpeg_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = metadata_body['downloadUrl'] + download_file_url = metadata_body['webContentLink'] aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.MAGIC_REVISION) @@ -954,26 +1021,29 @@ class TestMetadata: @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_metadata_file_root(self, provider, root_provider_fixtures): - file_metadata = root_provider_fixtures['list_file']['items'][0] + file_metadata = root_provider_fixtures['list_file']['files'][0] path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], file_metadata['id'])) - list_file_url = provider.build_url('files', path.identifier) - aiohttpretty.register_json_uri('GET', list_file_url, body=file_metadata) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=file_metadata) result = await provider.metadata(path) expected = GoogleDriveFileMetadata(file_metadata, path) assert result == expected + assert aiohttpretty.has_call(method='GET', uri=metadata_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_metadata_string_error_response(self, provider, root_provider_fixtures): path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], - root_provider_fixtures['list_file']['items'][0]['id'])) + root_provider_fixtures['list_file']['files'][0]['id'])) - list_file_url = provider.build_url('files', path.identifier) - aiohttpretty.register_uri('GET', list_file_url, headers={'Content-Type': 'text/html'}, + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_uri('GET', metadata_url, headers={'Content-Type': 'text/html'}, body='this is an error message string with a 404... or is it?', status=404) with pytest.raises(exceptions.NotFoundError) as e: @@ -981,6 +1051,7 @@ async def test_metadata_string_error_response(self, provider, root_provider_fixt assert e.value.code == 404 assert e.value.message == 'Could not retrieve file or directory {}'.format('/' + path.path) + assert aiohttpretty.has_call(method='GET', uri=metadata_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1000,33 +1071,35 @@ async def test_metadata_file_nested(self, provider): _ids=[str(x) for x in range(4)] ) - item = generate_list(3)['items'][0] - url = provider.build_url('files', path.identifier) + file = generate_list(3)['files'][0] - aiohttpretty.register_json_uri('GET', url, body=item) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=file) result = await provider.metadata(path) - expected = GoogleDriveFileMetadata(item, path) + expected = GoogleDriveFileMetadata(file, path) assert result == expected - assert aiohttpretty.has_call(method='GET', uri=url) + assert aiohttpretty.has_call(method='GET', uri=metadata_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_metadata_root_folder(self, provider, root_provider_fixtures): path = await provider.validate_path('/') - query = provider._build_query(provider.folder['id']) - list_file_url = provider.build_url('files', q=query, alt='json', maxResults=1000) - aiohttpretty.register_json_uri('GET', list_file_url, - body=root_provider_fixtures['list_file']) + query = provider._build_query(path.identifier) + folder_furl = furl.furl(provider.build_url('files', q=query, alt='json', pageSize=1000)) + folder_url = folder_furl.add(provider.FOLDER_FIELDS).url + aiohttpretty.register_json_uri('GET', folder_url, body=root_provider_fixtures['list_file']) result = await provider.metadata(path) expected = GoogleDriveFileMetadata( - root_provider_fixtures['list_file']['items'][0], - path.child(root_provider_fixtures['list_file']['items'][0]['title']) + root_provider_fixtures['list_file']['files'][0], + path.child(root_provider_fixtures['list_file']['files'][0]['name']) ) assert result == [expected] + assert aiohttpretty.has_call(method='GET', uri=folder_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1037,21 +1110,18 @@ async def test_metadata_folder_nested(self, provider): ) body = generate_list(3) - item = body['items'][0] + file = body['files'][0] query = provider._build_query(path.identifier) - url = provider.build_url('files', q=query, alt='json', maxResults=1000) - url_children = provider.build_url('files', q="'{}' in parents".format(path.identifier)) - - aiohttpretty.register_json_uri('GET', url, body=body) - aiohttpretty.register_json_uri('GET', url_children, body={'items': []}) + folder_furl = furl.furl(provider.build_url('files', q=query, alt='json', pageSize=1000)) + folder_url = folder_furl.add(provider.FOLDER_FIELDS).url + aiohttpretty.register_json_uri('GET', folder_url, body=body) result = await provider.metadata(path) - expected = GoogleDriveFileMetadata(item, path.child(item['title'])) - + expected = GoogleDriveFileMetadata(file, path.child(file['name'])) assert result == [expected] - assert aiohttpretty.has_call(method='GET', uri=url) + assert aiohttpretty.has_call(method='GET', uri=folder_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1062,19 +1132,19 @@ async def test_folder_metadata(self, provider, root_provider_fixtures): ) body = generate_list(3, **root_provider_fixtures['folder_metadata']) - item = body['items'][0] + file = body['files'][0] query = provider._build_query(path.identifier) - url = provider.build_url('files', q=query, alt='json', maxResults=1000) - - aiohttpretty.register_json_uri('GET', url, body=body) + folder_furl = furl.furl(provider.build_url('files', q=query, alt='json', pageSize=1000)) + folder_url = folder_furl.add(provider.FOLDER_FIELDS).url + aiohttpretty.register_json_uri('GET', folder_url, body=body) result = await provider.metadata(path) - expected = GoogleDriveFolderMetadata(item, path.child(item['title'], folder=True)) + expected = GoogleDriveFolderMetadata(file, path.child(file['name'], folder=True)) assert result == [expected] - assert aiohttpretty.has_call(method='GET', uri=url) + assert aiohttpretty.has_call(method='GET', uri=folder_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1085,8 +1155,8 @@ async def test_metadata_editable_gdoc_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) revisions_body = sharing_fixtures['editable_gdoc']['revisions'] @@ -1096,7 +1166,7 @@ async def test_metadata_editable_gdoc_no_revision(self, provider, sharing_fixtur result = await provider.metadata(path) local_metadata = copy.deepcopy(metadata_body) - local_metadata['version'] = revisions_body['items'][-1]['id'] + local_metadata['version'] = revisions_body['revisions'][-1]['id'] expected = GoogleDriveFileMetadata(local_metadata, path) assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1112,8 +1182,9 @@ async def test_metadata_editable_gdoc_good_revision(self, provider, sharing_fixt ) revision_body = sharing_fixtures['editable_gdoc']['revision'] - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.GDOC_GOOD_REVISION) + revision_furl = furl.furl(provider.build_url('files', path.identifier, + 'revisions', self.GDOC_GOOD_REVISION)) + revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) result = await provider.metadata(path, revision=self.GDOC_GOOD_REVISION) @@ -1132,14 +1203,16 @@ async def test_metadata_editable_gdoc_bad_revision(self, provider, sharing_fixtu ) no_such_revision_error = make_no_such_revision_error(self.GDOC_BAD_REVISION) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.GDOC_BAD_REVISION) + revision_furl = furl.furl(provider.build_url('files', path.identifier, + 'revisions', self.GDOC_BAD_REVISION)) + revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) with pytest.raises(exceptions.NotFoundError) as e: await provider.metadata(path, revision=self.GDOC_BAD_REVISION) assert e.value.code == 404 + assert aiohttpretty.has_call(method='GET', uri=revision_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1150,8 +1223,8 @@ async def test_metadata_editable_gdoc_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) revisions_body = sharing_fixtures['editable_gdoc']['revisions'] @@ -1161,7 +1234,8 @@ async def test_metadata_editable_gdoc_magic_revision(self, provider, sharing_fix result = await provider.metadata(path, revision=self.MAGIC_REVISION) local_metadata = copy.deepcopy(metadata_body) - local_metadata['version'] = revisions_body['items'][-1]['id'] + local_metadata['version'] = revisions_body['revisions'][-1]['id'] + expected = GoogleDriveFileMetadata(local_metadata, path) assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1176,15 +1250,17 @@ async def test_metadata_viewable_gdoc_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) result = await provider.metadata(path) local_metadata = copy.deepcopy(metadata_body) - local_metadata['version'] = local_metadata['etag'] + ds.DRIVE_IGNORE_VERSION + etag = '{}::{}'.format(local_metadata['id'], local_metadata['modifiedTime']) + local_metadata['version'] = etag + ds.DRIVE_IGNORE_VERSION expected = GoogleDriveFileMetadata(local_metadata, path) + assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1198,8 +1274,9 @@ async def test_metadata_viewable_gdoc_bad_revision(self, provider, sharing_fixtu ) unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.GDOC_BAD_REVISION) + revision_furl = furl.furl(provider.build_url('files', path.identifier, + 'revisions', self.GDOC_BAD_REVISION)) + revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) with pytest.raises(exceptions.NotFoundError) as e: @@ -1216,14 +1293,15 @@ async def test_metadata_viewable_gdoc_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) result = await provider.metadata(path, revision=self.MAGIC_REVISION) local_metadata = copy.deepcopy(metadata_body) - local_metadata['version'] = local_metadata['etag'] + ds.DRIVE_IGNORE_VERSION + etag = '{}::{}'.format(local_metadata['id'], local_metadata['modifiedTime']) + local_metadata['version'] = etag + ds.DRIVE_IGNORE_VERSION expected = GoogleDriveFileMetadata(local_metadata, path) assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1237,8 +1315,8 @@ async def test_metadata_editable_jpeg_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) result = await provider.metadata(path) @@ -1257,8 +1335,9 @@ async def test_metadata_editable_jpeg_good_revision(self, provider, sharing_fixt ) revision_body = sharing_fixtures['editable_jpeg']['revision'] - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.JPEG_GOOD_REVISION) + revision_furl = furl.furl(provider.build_url('files', path.identifier, + 'revisions', self.JPEG_GOOD_REVISION)) + revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) result = await provider.metadata(path, revision=self.JPEG_GOOD_REVISION) @@ -1277,8 +1356,9 @@ async def test_metadata_editable_jpeg_bad_revision(self, provider, sharing_fixtu ) no_such_revision_error = make_no_such_revision_error(self.JPEG_BAD_REVISION) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.JPEG_BAD_REVISION) + revision_furl = furl.furl(provider.build_url('files', path.identifier, + 'revisions', self.JPEG_BAD_REVISION)) + revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) with pytest.raises(exceptions.NotFoundError) as e: @@ -1295,8 +1375,8 @@ async def test_metadata_editable_jpeg_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) result = await provider.metadata(path, revision=self.MAGIC_REVISION) @@ -1314,8 +1394,8 @@ async def test_metadata_viewable_jpeg_no_revision(self, provider, sharing_fixtur _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) result = await provider.metadata(path) @@ -1334,8 +1414,9 @@ async def test_metadata_viewable_jpeg_bad_revision(self, provider, sharing_fixtu ) unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.JPEG_BAD_REVISION) + revision_furl = furl.furl(provider.build_url('files', path.identifier, + 'revisions', self.JPEG_BAD_REVISION)) + revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) with pytest.raises(exceptions.NotFoundError) as e: @@ -1352,8 +1433,8 @@ async def test_metadata_viewable_jpeg_magic_revision(self, provider, sharing_fix _ids=['1', '2', metadata_body['id']] ) - metadata_query = provider._build_query(path.identifier) - metadata_url = provider.build_url('files', path.identifier) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) result = await provider.metadata(path, revision=self.MAGIC_REVISION) @@ -1368,17 +1449,17 @@ class TestRevisions: @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_get_revisions(self, provider, revision_fixtures, root_provider_fixtures): - item = root_provider_fixtures['list_file']['items'][0] - path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', item['id'])) + file = root_provider_fixtures['list_file']['files'][0] + path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', file['id'])) - revisions_url = provider.build_url('files', item['id'], 'revisions') + revisions_url = provider.build_url('files', file['id'], 'revisions') aiohttpretty.register_json_uri('GET', revisions_url, body=revision_fixtures['revisions_list']) result = await provider.revisions(path) expected = [ GoogleDriveRevision(each) - for each in revision_fixtures['revisions_list']['items'] + for each in revision_fixtures['revisions_list']['revisions'] ] assert result == expected @@ -1386,20 +1467,24 @@ async def test_get_revisions(self, provider, revision_fixtures, root_provider_fi @pytest.mark.aiohttpretty async def test_get_revisions_no_revisions(self, provider, revision_fixtures, root_provider_fixtures): - item = root_provider_fixtures['list_file']['items'][0] - metadata_url = provider.build_url('files', item['id']) - revisions_url = provider.build_url('files', item['id'], 'revisions') - path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', item['id'])) + file = root_provider_fixtures['list_file']['files'][0] + path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', file['id'])) + + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=file) + + revisions_url = provider.build_url('files', file['id'], 'revisions') - aiohttpretty.register_json_uri('GET', metadata_url, body=item) aiohttpretty.register_json_uri('GET', revisions_url, body=revision_fixtures['revisions_list_empty']) result = await provider.revisions(path) + etag = '{}::{}'.format(file['id'], file['modifiedTime']) expected = [ GoogleDriveRevision({ - 'modifiedDate': item['modifiedDate'], - 'id': item['etag'] + ds.DRIVE_IGNORE_VERSION, + 'modifiedTime': file['modifiedTime'], + 'id': etag + ds.DRIVE_IGNORE_VERSION, }) ] assert result == expected @@ -1408,20 +1493,23 @@ async def test_get_revisions_no_revisions(self, provider, revision_fixtures, @pytest.mark.aiohttpretty async def test_get_revisions_for_uneditable(self, provider, sharing_fixtures): file_fixtures = sharing_fixtures['viewable_gdoc'] - item = file_fixtures['metadata'] - metadata_url = provider.build_url('files', item['id']) - revisions_url = provider.build_url('files', item['id'], 'revisions') - path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', item['id'])) + file = file_fixtures['metadata'] + path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', file['id'])) + + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('GET', metadata_url, body=file) - aiohttpretty.register_json_uri('GET', metadata_url, body=item) + revisions_url = provider.build_url('files', file['id'], 'revisions') aiohttpretty.register_json_uri( 'GET', revisions_url, body=file_fixtures['revisions_error'], status=403) result = await provider.revisions(path) + etag = '{}::{}'.format(file['id'], file['modifiedTime']) expected = [ GoogleDriveRevision({ - 'modifiedDate': item['modifiedDate'], - 'id': item['etag'] + ds.DRIVE_IGNORE_VERSION, + 'modifiedTime': file['modifiedTime'], + 'id': etag + ds.DRIVE_IGNORE_VERSION, }) ] assert result == expected @@ -1452,11 +1540,14 @@ async def test_already_exists(self, provider): async def test_returns_metadata(self, provider, root_provider_fixtures): path = WaterButlerPath('/osf%20test/', _ids=(provider.folder['id'], None)) - aiohttpretty.register_json_uri('POST', provider.build_url('files'), + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('POST', metadata_url, body=root_provider_fixtures['folder_metadata']) resp = await provider.create_folder(path) + assert aiohttpretty.has_call(method='POST', uri=metadata_url) assert resp.kind == 'folder' assert resp.name == 'osf test' assert resp.path == '/osf%20test/' @@ -1467,8 +1558,9 @@ async def test_raises_non_404(self, provider): path = WaterButlerPath('/hugo/kim/pins/', _ids=(provider.folder['id'], 'something', 'something', None)) - url = provider.build_url('files') - aiohttpretty.register_json_uri('POST', url, status=418) + metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) + metadata_url = metadata_furl.add(provider.FILE_FIELDS).url + aiohttpretty.register_json_uri('POST', metadata_url, status=418) with pytest.raises(exceptions.CreateFolderError) as e: await provider.create_folder(path) @@ -1487,134 +1579,131 @@ class TestIntraFunctions: @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_intra_move_file(self, provider, root_provider_fixtures): - item = root_provider_fixtures['docs_file_metadata'] - src_path = WaterButlerPath('/unsure.txt', _ids=(provider.folder['id'], item['id'])) + file = root_provider_fixtures['docs_file_metadata'] + src_path = WaterButlerPath('/unsure.txt', _ids=(provider.folder['id'], file['id'])) dest_path = WaterButlerPath('/really/unsure.txt', _ids=(provider.folder['id'], - item['id'], item['id'])) + file['id'], file['id'])) - url = provider.build_url('files', src_path.identifier) + move_furl = furl.furl(provider.build_url('files', src_path.identifier, + removeParents=src_path.parent.identifier, + addParents=dest_path.parent.identifier)) + move_url = move_furl.add(provider.FILE_FIELDS).url data = json.dumps({ - 'parents': [{ - 'id': dest_path.parent.identifier - }], - 'title': dest_path.name + 'name': dest_path.name }), - aiohttpretty.register_json_uri('PATCH', url, data=data, body=item) + aiohttpretty.register_json_uri('PATCH', move_url, data=data, body=file) - delete_url = provider.build_url('files', item['id']) - del_url_body = json.dumps({'labels': {'trashed': 'true'}}) - aiohttpretty.register_uri('PUT', delete_url, body=del_url_body, status=200) + delete_url = provider.build_url('files', file['id']) + del_url_body = json.dumps({'trashed': 'true'}) + aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) result, created = await provider.intra_move(provider, src_path, dest_path) - expected = GoogleDriveFileMetadata(item, dest_path) + expected = GoogleDriveFileMetadata(file, dest_path) assert result == expected - assert aiohttpretty.has_call(method='PUT', uri=delete_url) + assert aiohttpretty.has_call(method='PATCH', uri=move_url) + assert aiohttpretty.has_call(method='PATCH', uri=delete_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_intra_move_folder(self, provider, root_provider_fixtures): - item = root_provider_fixtures['folder_metadata'] - src_path = WaterButlerPath('/unsure/', _ids=(provider.folder['id'], item['id'])) + folder = root_provider_fixtures['folder_metadata'] + folder2 = root_provider_fixtures['folder2_metadata'] + src_path = WaterButlerPath('/unsure/', _ids=(provider.folder['id'], folder['id'])) dest_path = WaterButlerPath('/really/unsure/', _ids=(provider.folder['id'], - item['id'], item['id'])) + folder2['id'], folder2['id'])) - url = provider.build_url('files', src_path.identifier) + move_furl = furl.furl(provider.build_url('files', src_path.identifier, + removeParents=src_path.parent.identifier, + addParents=dest_path.parent.identifier)) + move_url = move_furl.add(provider.FILE_FIELDS).url data = json.dumps({ - 'parents': [{ - 'id': dest_path.parent.identifier - }], - 'title': dest_path.name + 'name': dest_path.name }), - aiohttpretty.register_json_uri('PATCH', url, data=data, body=item) + aiohttpretty.register_json_uri('PATCH', move_url, data=data, body=folder) - delete_url = provider.build_url('files', item['id']) - del_url_body = json.dumps({'labels': {'trashed': 'true'}}) - aiohttpretty.register_uri('PUT', delete_url, body=del_url_body, status=200) + delete_url = provider.build_url('files', folder2['id']) + del_url_body = json.dumps({'trashed': 'true'}) + aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) + + query = provider._build_query(src_path.identifier) + children_furl = furl.furl(provider.build_url('files', q=query, alt='json', pageSize=1000)) + children_url = children_furl.add(provider.FOLDER_FIELDS).url - children_query = provider._build_query(dest_path.identifier) - children_url = provider.build_url('files', q=children_query, alt='json', maxResults=1000) children_list = generate_list(3, **root_provider_fixtures['folder_metadata']) aiohttpretty.register_json_uri('GET', children_url, body=children_list) result, created = await provider.intra_move(provider, src_path, dest_path) - expected = GoogleDriveFolderMetadata(item, dest_path) + expected = GoogleDriveFolderMetadata(folder, dest_path) expected.children = [ - provider._serialize_item(dest_path.child(item['title']), item) - for item in children_list['items'] + provider._serialize_file(dest_path.child(file['name']), file) + for file in children_list['files'] ] assert result == expected - assert aiohttpretty.has_call(method='PUT', uri=delete_url) + assert aiohttpretty.has_call(method='PATCH', uri=move_url) + assert aiohttpretty.has_call(method='PATCH', uri=delete_url) @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_intra_copy_file(self, provider, root_provider_fixtures): - item = root_provider_fixtures['docs_file_metadata'] - src_path = WaterButlerPath('/unsure.txt', _ids=(provider.folder['id'], item['id'])) + file = root_provider_fixtures['docs_file_metadata'] + src_path = WaterButlerPath('/unsure.txt', _ids=(provider.folder['id'], file['id'])) dest_path = WaterButlerPath('/really/unsure.txt', _ids=(provider.folder['id'], - item['id'], item['id'])) + file['id'], file['id'])) - url = provider.build_url('files', src_path.identifier, 'copy') + copy_furl = furl.furl(provider.build_url('files', src_path.identifier, 'copy')) + copy_url = copy_furl.add(provider.FILE_FIELDS).url data = json.dumps({ 'parents': [{ 'id': dest_path.parent.identifier }], - 'title': dest_path.name + 'name': dest_path.name }), - aiohttpretty.register_json_uri('POST', url, data=data, body=item) + aiohttpretty.register_json_uri('POST', copy_url, data=data, body=file) - delete_url = provider.build_url('files', item['id']) - del_url_body = json.dumps({'labels': {'trashed': 'true'}}) - aiohttpretty.register_uri('PUT', delete_url, body=del_url_body, status=200) + delete_url = provider.build_url('files', file['id']) + del_url_body = json.dumps({'trashed': 'true'}) + aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) result, created = await provider.intra_copy(provider, src_path, dest_path) - expected = GoogleDriveFileMetadata(item, dest_path) + expected = GoogleDriveFileMetadata(file, dest_path) assert result == expected - assert aiohttpretty.has_call(method='PUT', uri=delete_url) + assert aiohttpretty.has_call(method='POST', uri=copy_url) + assert aiohttpretty.has_call(method='PATCH', uri=delete_url) class TestOperationsOrMisc: - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_duplicate_names(self, provider): + def test_can_duplicate_names(self, provider): assert provider.can_duplicate_names() is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_shares_storage_root(self, provider, other_provider): + def test_shares_storage_root(self, provider, other_provider): assert provider.shares_storage_root(other_provider) is True assert provider.shares_storage_root(provider) is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_intra_move(self, provider, other_provider): + def test_can_intra_move(self, provider, other_provider): assert provider.can_intra_move(other_provider) is False assert provider.can_intra_move(provider) is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test__serialize_item_raw(self, provider, root_provider_fixtures): - item = root_provider_fixtures['docs_file_metadata'] + def test_serialize_item_raw(self, provider, root_provider_fixtures): + file = root_provider_fixtures['docs_file_metadata'] - assert provider._serialize_item(None, item, True) == item + assert provider._serialize_file(None, file, True) == file - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_intra_copy(self, provider, other_provider, root_provider_fixtures): - item = root_provider_fixtures['list_file']['items'][0] - path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], item['id'])) + def test_can_intra_copy(self, provider, other_provider, root_provider_fixtures): + file = root_provider_fixtures['list_file']['files'][0] + path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], file['id'])) assert provider.can_intra_copy(other_provider, path) is False assert provider.can_intra_copy(provider, path) is True def test_path_from_metadata(self, provider, root_provider_fixtures): - item = root_provider_fixtures['docs_file_metadata'] - src_path = WaterButlerPath('/version-test.docx', _ids=(provider.folder['id'], item['id'])) + file = root_provider_fixtures['docs_file_metadata'] + src_path = WaterButlerPath('/version-test.docx', _ids=(provider.folder['id'], file['id'])) - metadata = GoogleDriveFileMetadata(item, src_path) + metadata = GoogleDriveFileMetadata(file, src_path) child_path = provider.path_from_metadata(src_path.parent, metadata) assert child_path.full_path == src_path.full_path @@ -1625,18 +1714,17 @@ def test_path_from_metadata(self, provider, root_provider_fixtures): async def test_revalidate_path_file_error(self, provider, root_provider_fixtures, error_fixtures): file_name = '/root/whatever/Gear1.stl' - file_id = root_provider_fixtures['revalidate_path_file_metadata_1']['items'][0]['id'] + file_id = root_provider_fixtures['revalidate_path_file_metadata_1']['files'][0]['id'] path = GoogleDrivePath(file_name, _ids=['0', file_id, file_id, file_id]) parts = [[parse.unquote(x), True] for x in file_name.strip('/').split('/')] parts[-1][1] = False current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] - query = _build_title_search_query(provider, part_name, True) + query = _build_name_search_query(provider, part_name, provider.folder['id'], True) - url = provider.build_url('files', provider.folder['id'], 'children', - q=query, fields='items(id)') - aiohttpretty.register_json_uri('GET', url, + list_url = provider.build_url('files', q=query, fields='files(id)') + aiohttpretty.register_json_uri('GET', list_url, body=error_fixtures['parts_file_missing_metadata']) with pytest.raises(exceptions.MetadataError) as e: diff --git a/waterbutler/providers/googledrive/metadata.py b/waterbutler/providers/googledrive/metadata.py index 5e4fbafa8..6981cfcd1 100644 --- a/waterbutler/providers/googledrive/metadata.py +++ b/waterbutler/providers/googledrive/metadata.py @@ -38,7 +38,7 @@ def id(self): @property def name(self): - return self.raw['title'] + return self.raw['name'] @property def path(self): @@ -55,10 +55,10 @@ def export_name(self): class GoogleDriveFileMetadata(BaseGoogleDriveMetadata, metadata.BaseFileMetadata): """The metadata for a single file on Google Drive. This class expects a the ``raw`` - property to be the response[1] from the GDrive v2 file metadata endpoint[2]. + property to be the response[1] from the GDrive v3 file metadata endpoint[2]. - [1] https://developers.google.com/drive/v2/reference/files - [2] https://developers.google.com/drive/v2/reference/files/get + [1] https://developers.google.com/drive/v3/reference/files + [2] https://developers.google.com/drive/v3/reference/files/get """ @property @@ -67,11 +67,11 @@ def id(self): @property def name(self): - title = self._file_title + name = self._file_name if self.is_google_doc: ext = utils.get_extension(self.raw) - title += ext - return title + name += ext + return name @property def path(self): @@ -92,15 +92,15 @@ def materialized_path(self): @property def size(self): # Google docs(Docs,sheets, slides, etc) don't have file size before they are exported - return self.raw.get('fileSize') + return self.raw.get('size') @property def modified(self): - return self.raw['modifiedDate'] + return self.raw['modifiedTime'] @property def created_utc(self): - return core_utils.normalize_datetime(self.raw['createdDate']) + return core_utils.normalize_datetime(self.raw['createdTime']) @property def content_type(self): @@ -113,7 +113,7 @@ def etag(self): @property def extra(self): ret = super().extra - ret['webView'] = self.raw.get('alternateLink') + ret['webView'] = self.raw.get('webViewLink') if self.is_google_doc: ret['downloadExt'] = utils.get_download_extension(self.raw) @@ -126,31 +126,31 @@ def extra(self): @property def is_google_doc(self): - return utils.is_docs_file(self.raw) is not None + return utils.is_docs_file(self.raw) @property def export_name(self): - title = self._file_title + name = self._file_name if self.is_google_doc: ext = utils.get_download_extension(self.raw) - title += ext - return title + name += ext + return name @property - def _file_title(self): - return self.raw['title'] + def _file_name(self): + return self.raw['name'] class GoogleDriveFileRevisionMetadata(GoogleDriveFileMetadata): """The metadata for a single file at a particular revision on Google Drive. This class expects - the ``raw`` property to be the response[1] from the GDrive v2 revision metadata endpoint[2]. + the ``raw`` property to be the response[1] from the GDrive v3 revision metadata endpoint[2]. This response is similar to the one from the file metadata endpoint, but lacks a created date and version field. It also stores the file name of non-GDoc files in the ``originalFilename`` - field instead of the ``title`` field. GDocs do not include the file name at all, and must + field instead of the ``name`` field. GDocs do not include the file name at all, and must derive it from the `GoogleDrivePath` object. - [1] https://developers.google.com/drive/v2/reference/revisions - [2] https://developers.google.com/drive/v2/reference/revisions/get + [1] https://developers.google.com/drive/v3/files/fileId/revisions + [2] https://developers.google.com/drive/v3/files/fileId/revisions/revisionId """ @property @@ -159,7 +159,8 @@ def created_utc(self): @property def etag(self): - return self.raw['etag'] + # Google Doc revision representations do not return etag + return '{}::{}'.format(self.raw['id'], self.raw['modifiedTime']) @property def extra(self): @@ -171,7 +172,7 @@ def extra(self): return {'md5': self.raw['md5Checksum']} @property - def _file_title(self): + def _file_name(self): return self.raw.get('originalFilename', self._path.name) @@ -187,4 +188,4 @@ def version(self): @property def modified(self): - return self.raw['modifiedDate'] + return self.raw['modifiedTime'] diff --git a/waterbutler/providers/googledrive/provider.py b/waterbutler/providers/googledrive/provider.py index 73712d601..a5f90094e 100644 --- a/waterbutler/providers/googledrive/provider.py +++ b/waterbutler/providers/googledrive/provider.py @@ -22,7 +22,7 @@ GoogleDriveRevision) -def clean_query(query: str): +def clean_query(query: str) -> str: # Replace \ with \\ and ' with \' # Note only single quotes need to be escaped return query.replace('\\', r'\\').replace("'", r"\'") @@ -41,10 +41,9 @@ class GoogleDrivePath(wb_path.WaterButlerPath): class GoogleDriveProvider(provider.BaseProvider): """Provider for Google's Drive cloud storage service. - This provider uses the v2 Drive API. A v3 API is available, but this provider has not yet - been updated. + This provider uses the v3 Drive API. - API docs: https://developers.google.com/drive/v2/reference/ + API docs: https://developers.google.com/drive/v3/reference/ Quirks: @@ -75,10 +74,14 @@ class GoogleDriveProvider(provider.BaseProvider): NAME = 'googledrive' BASE_URL = settings.BASE_URL FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder' + FILE_FIELDS = {'fields': 'version, id, name, size, modifiedTime, createdTime, mimeType, \ + webViewLink, webContentLink, md5Checksum, capabilities(canDelete, canEdit, \ + canTrash, canDownload, canRename, canReadRevisions, canShare, canCopy)'} + FOLDER_FIELDS = {'fields': 'files({})'.format(FILE_FIELDS['fields'])} + REVISION_FIELDS = {'fields': 'id, originalFilename, size, modifiedTime, mimeType, md5Checksum'} - # https://developers.google.com/drive/v2/web/about-permissions#roles + # https://developers.google.com/drive/v3/web/about-permissions#roles # 'reader' and 'commenter' are not authorized to access the revisions list - ROLES_ALLOWING_REVISIONS = ['owner', 'organizer', 'writer'] def __init__(self, auth: dict, credentials: dict, settings: dict) -> None: super().__init__(auth, credentials, settings) @@ -95,7 +98,7 @@ async def validate_v1_path(self, path: str, **kwargs) -> GoogleDrivePath: if parts[-1]['id'] is None or implicit_folder != explicit_folder: raise exceptions.NotFoundError(str(path)) - names, ids = zip(*[(parse.quote(x['title'], safe=''), x['id']) for x in parts]) + names, ids = zip(*[(parse.quote(x['name'], safe=''), x['id']) for x in parts]) return GoogleDrivePath('/'.join(names), _ids=ids, folder='folder' in parts[-1]['mimeType']) async def validate_path(self, path: str, **kwargs) -> GoogleDrivePath: @@ -103,7 +106,7 @@ async def validate_path(self, path: str, **kwargs) -> GoogleDrivePath: return GoogleDrivePath('/', _ids=[self.folder['id']], folder=True) parts = await self._resolve_path_to_ids(path) - names, ids = zip(*[(parse.quote(x['title'], safe=''), x['id']) for x in parts]) + names, ids = zip(*[(parse.quote(x['name'], safe=''), x['id']) for x in parts]) return GoogleDrivePath('/'.join(names), _ids=ids, folder='folder' in parts[-1]['mimeType']) async def revalidate_path(self, @@ -121,20 +124,20 @@ async def revalidate_path(self, name += '/' parts = await self._resolve_path_to_ids(name, start_at=[{ - 'title': base.name, + 'name': base.name, 'mimeType': 'folder', 'id': base.identifier, }]) - _id, name, mime = list(map(parts[-1].__getitem__, ('id', 'title', 'mimeType'))) + _id, name, mime = list(map(parts[-1].__getitem__, ('id', 'name', 'mimeType'))) return base.child(name, _id=_id, folder='folder' in mime) - def can_duplicate_names(self) -> bool: - return True - @property def default_headers(self) -> dict: return {'authorization': 'Bearer {}'.format(self.token)} + def can_duplicate_names(self) -> bool: + return True + def can_intra_move(self, other: provider.BaseProvider, path=None) -> bool: @@ -155,17 +158,19 @@ async def intra_move(self, # type: ignore if dest_path.identifier: await dest_provider.delete(dest_path) + request_furl = furl.furl(self.build_url('files', src_path.identifier, + removeParents=src_path.parent.identifier, + addParents=dest_path.parent.identifier)) + request_url = request_furl.add(self.FILE_FIELDS).url + async with self.request( 'PATCH', - self.build_url('files', src_path.identifier), + request_url, headers={ 'Content-Type': 'application/json' }, data=json.dumps({ - 'parents': [{ - 'id': dest_path.parent.identifier - }], - 'title': dest_path.name + 'name': dest_path.name }), expects=(200, ), throws=exceptions.IntraMoveError, @@ -191,15 +196,16 @@ async def intra_copy(self, if dest_path.identifier: await dest_provider.delete(dest_path) + request_furl = furl.furl(self.build_url('files', src_path.identifier, 'copy')) + request_url = request_furl.add(self.FILE_FIELDS).url + async with self.request( 'POST', - self.build_url('files', src_path.identifier, 'copy'), + request_url, headers={'Content-Type': 'application/json'}, data=json.dumps({ - 'parents': [{ - 'id': dest_path.parent.identifier - }], - 'title': dest_path.name + 'parents': [dest_path.parent.identifier], + 'name': dest_path.name }), expects=(200, ), throws=exceptions.IntraMoveError, @@ -232,11 +238,49 @@ async def download(self, # type: ignore :returns: For GDocs, a StringStream. All others, a ResponseStreamReader. """ - metadata = await self.metadata(path, revision=revision) + metadata = await self.metadata(path) + if revision and revision.endswith(settings.DRIVE_IGNORE_VERSION): + revision = None + is_docs_file = drive_utils.is_docs_file(metadata.raw) + # GoogleDrive v3 API does not allow the download of GoogleDoc revisions + # Use v2 API for this functionality + if revision and is_docs_file: + meta_url = self.build_url('files', path.identifier, 'revisions', revision) + meta_url = meta_url.replace('/v3/', '/v2/', 1) + + async with self.request( + 'GET', meta_url, + expects=(200, 403, 404, ), + throws=exceptions.MetadataError, + ) as resp: + try: + data = await resp.json() + except: # some 404s return a string instead of json + data = await resp.read() + + if resp.status != 200: + raise exceptions.NotFoundError(path) + + format = drive_utils.get_format(data) + url = data['exportLinks'][format['export_mimetype']] + # Is a revision but not a Docs file + elif revision: + url = self.build_url('files', metadata.raw.get('id'), + 'revisions', revision, alt='media') + # Is neither a revision nor a Docs file + elif not is_docs_file: + # webContentLink is only available for files with binary content. + # e.g. Not GoogleDocs + url = metadata.raw.get('webContentLink') + # Is a Docs file but not a revision + else: + ext = os.path.splitext(metadata.name)[-1] + mime_type = drive_utils.get_export_mimetype_from_ext(ext) + url = self.build_url('files', metadata.raw.get('id'), 'export', mimeType=mime_type) download_resp = await self.make_request( 'GET', - metadata.raw.get('downloadUrl') or drive_utils.get_export_link(metadata.raw), # type: ignore + url, range=range, expects=(200, 206), throws=exceptions.DownloadError, @@ -307,24 +351,24 @@ async def delete(self, # type: ignore ) async with self.request( - 'PUT', + 'PATCH', self.build_url('files', path.identifier), - data=json.dumps({'labels': {'trashed': 'true'}}), + data=json.dumps({'trashed': 'true'}), headers={'Content-Type': 'application/json'}, expects=(200, ), throws=exceptions.DeleteError, ): return - def _build_query(self, folder_id: str, title: str=None) -> str: + def _build_query(self, folder_id: str, name: str=None) -> str: queries = [ "'{}' in parents".format(folder_id), 'trashed = false', "mimeType != 'application/vnd.google-apps.form'", "mimeType != 'application/vnd.google-apps.map'", ] - if title: - queries.append("title = '{}'".format(clean_query(title))) + if name: + queries.append("name = '{}'".format(clean_query(name))) return ' and '.join(queries) async def metadata(self, # type: ignore @@ -367,17 +411,20 @@ async def revisions(self, path: GoogleDrivePath, **kwargs) -> typing.List[Google data = await resp.json() has_revisions = resp.status == 200 - if has_revisions and data['items']: + if has_revisions and data['revisions']: return [ - GoogleDriveRevision(item) - for item in reversed(data['items']) + GoogleDriveRevision(revision) + for revision in reversed(data['revisions']) ] # Use dummy ID if no revisions found metadata = await self.metadata(path, raw=True) + # GoogleDrive does not return etag for GoogleDocs + etag = metadata.get('etag', None) or '{}::{}'.format(metadata['id'], + metadata['modifiedTime']) return [GoogleDriveRevision({ - 'modifiedDate': metadata['modifiedDate'], # type: ignore - 'id': metadata['etag'] + settings.DRIVE_IGNORE_VERSION, + 'modifiedTime': metadata['modifiedTime'], # type: ignore + 'id': etag + settings.DRIVE_IGNORE_VERSION, })] async def create_folder(self, @@ -390,17 +437,17 @@ async def create_folder(self, if path.identifier: raise exceptions.FolderNamingConflict(path.name) + request_furl = furl.furl(self.build_url('files')) + request_url = request_furl.add(self.FILE_FIELDS).url async with self.request( 'POST', - self.build_url('files'), + request_url, headers={ 'Content-Type': 'application/json', }, data=json.dumps({ - 'title': path.name, - 'parents': [{ - 'id': path.parent.identifier - }], + 'name': path.name, + 'parents': [path.parent.identifier], 'mimeType': self.FOLDER_MIME_TYPE, }), expects=(200, ), @@ -415,25 +462,20 @@ def path_from_metadata(self, parent_path, metadata): def _build_upload_url(self, *segments, **query): return provider.build_url(settings.BASE_UPLOAD_URL, *segments, **query) - def _serialize_item(self, + def _serialize_file(self, path: wb_path.WaterButlerPath, - item: dict, + file: dict, raw: bool=False) -> typing.Union[BaseGoogleDriveMetadata, dict]: if raw: - return item - if item['mimeType'] == self.FOLDER_MIME_TYPE: - return GoogleDriveFolderMetadata(item, path) - return GoogleDriveFileMetadata(item, path) + return file + if file['mimeType'] == self.FOLDER_MIME_TYPE: + return GoogleDriveFolderMetadata(file, path) + return GoogleDriveFileMetadata(file, path) def _build_upload_metadata(self, folder_id: str, name: str) -> dict: return { - 'parents': [ - { - 'kind': 'drive#parentReference', - 'id': folder_id, - }, - ], - 'title': name, + 'parents': [folder_id], + 'name': name, } async def _start_resumable_upload(self, @@ -441,9 +483,16 @@ async def _start_resumable_upload(self, segments: typing.Sequence[str], size, metadata: dict) -> str: + + if not created: + del metadata['parents'] + + request_furl = furl.furl(self._build_upload_url('files', *segments, + uploadType='resumable')) + request_url = request_furl.add(self.FILE_FIELDS).url async with self.request( - 'POST' if created else 'PUT', - self._build_upload_url('files', *segments, uploadType='resumable'), + 'POST' if created else 'PATCH', + request_url, headers={ 'Content-Type': 'application/json', 'X-Upload-Content-Length': str(size), @@ -469,15 +518,15 @@ async def _finish_resumable_upload(self, segments: typing.Sequence[str], stream, async def _resolve_path_to_ids(self, path, start_at=None): """Takes a path and traverses the file tree (ha!) beginning at ``start_at``, looking for something that matches ``path``. Returns a list of dicts for each part of the path, with - ``title``, ``mimeType``, and ``id`` keys. + ``name``, ``mimeType``, and ``id`` keys. """ self.metrics.incr('called_resolve_path_to_ids') ret = start_at or [{ - 'title': '', + 'name': '', 'mimeType': 'folder', 'id': self.folder['id'], }] - item_id = ret[0]['id'] + file_id = ret[0]['id'] # parts is list of [path_part_name, is_folder] parts = [[parse.unquote(x), True] for x in path.strip('/').split('/')] @@ -488,12 +537,13 @@ async def _resolve_path_to_ids(self, path, start_at=None): part_name, part_is_folder = current_part[0], current_part[1] name, ext = os.path.splitext(part_name) if not part_is_folder and ext in ('.gdoc', '.gdraw', '.gslides', '.gsheet'): - gd_ext = drive_utils.get_mimetype_from_ext(ext) - query = "title = '{}' " \ + gd_mimetype = drive_utils.get_mimetype_from_ext(ext) + query = "name = '{}' " \ "and trashed = false " \ - "and mimeType = '{}'".format(clean_query(name), gd_ext) + "and '{}' in parents " \ + "and mimeType = '{}'".format(clean_query(name), file_id, gd_mimetype) else: - query = "title = '{}' " \ + query = "name = '{}' " \ "and trashed = false " \ "and mimeType != 'application/vnd.google-apps.form' " \ "and mimeType != 'application/vnd.google-apps.map' " \ @@ -501,21 +551,23 @@ async def _resolve_path_to_ids(self, path, start_at=None): "and mimeType != 'application/vnd.google-apps.drawing' " \ "and mimeType != 'application/vnd.google-apps.presentation' " \ "and mimeType != 'application/vnd.google-apps.spreadsheet' " \ + "and '{}' in parents " \ "and mimeType {} '{}'".format( clean_query(part_name), + file_id, '=' if part_is_folder else '!=', self.FOLDER_MIME_TYPE ) async with self.request( 'GET', - self.build_url('files', item_id, 'children', q=query, fields='items(id)'), + self.build_url('files', q=query, fields='files(id)'), expects=(200, ), throws=exceptions.MetadataError, ) as resp: data = await resp.json() try: - item_id = data['items'][0]['id'] + file_id = data['files'][0]['id'] except (KeyError, IndexError): if parts: # if we can't find an intermediate path part, that's an error @@ -523,31 +575,30 @@ async def _resolve_path_to_ids(self, path, start_at=None): code=HTTPStatus.NOT_FOUND) return ret + [{ 'id': None, - 'title': part_name, + 'name': part_name, 'mimeType': 'folder' if part_is_folder else '', }] async with self.request( 'GET', - self.build_url('files', item_id, fields='id,title,mimeType'), + self.build_url('files', file_id, fields='id, name, mimeType'), expects=(200, ), throws=exceptions.MetadataError, ) as resp: ret.append(await resp.json()) return ret - async def _handle_docs_versioning(self, path: GoogleDrivePath, item: dict, raw: bool=True): + async def _handle_docs_versioning(self, path: GoogleDrivePath, file: dict, raw: bool=True): """Sends an extra request to GDrive to fetch revision information for Google Docs. Needed because Google Docs use a different versioning system from regular files. - I've been unable to replicate the case where revisions_data['items'] is None. I'm leaving - it in for now and adding a metric to see if we ever actually encounter this case. If not, - we should probably remove it to simplify this method. + I've been unable to replicate the case where revisions_data['revisions'] is None. I'm + leaving it in for now and adding a metric to see if we ever actually encounter this case. + If not, we should probably remove it to simplify this method. This method does not handle the case of read-only google docs, which will return a 403. - Other methods should check the ``userPermission.role`` field of the file metadata before - calling this. If the value of that field is ``"reader"`` or ``"commenter"``, this method - will error. + Other methods should check the ``capabilities.canReadRevisions`` field of the file metadata + before calling this. If the value of that field is ``"false"``, this method will error. :param GoogleDrivePath path: the path of the google doc to get version information for :param dict item: a raw response object from the GDrive file metadata endpoint @@ -558,28 +609,29 @@ async def _handle_docs_versioning(self, path: GoogleDrivePath, item: dict, raw: """ async with self.request( 'GET', - self.build_url('files', item['id'], 'revisions'), + self.build_url('files', file['id'], 'revisions'), expects=(200, ), throws=exceptions.RevisionsError, ) as resp: revisions_data = await resp.json() - has_revisions = revisions_data['items'] is not None + has_revisions = revisions_data['revisions'] is not None # Revisions are not available for some sharing configurations. If revisions list is empty, # use the etag of the file plus a sentinel string as a dummy revision ID. self.metrics.add('handle_docs_versioning.empty_revision_list', not has_revisions) if has_revisions: - item['version'] = revisions_data['items'][-1]['id'] + file['version'] = revisions_data['revisions'][-1]['id'] else: # If there are no revisions use etag as vid - item['version'] = item['etag'] + settings.DRIVE_IGNORE_VERSION + file['version'] = file['etag'] + settings.DRIVE_IGNORE_VERSION - return self._serialize_item(path, item, raw=raw) + return self._serialize_file(path, file, raw=raw) async def _folder_metadata(self, path: wb_path.WaterButlerPath, raw: bool=False) \ -> typing.List[typing.Union[BaseGoogleDriveMetadata, dict]]: query = self._build_query(path.identifier) - built_url = self.build_url('files', q=query, alt='json', maxResults=1000) + built_furl = furl.furl(self.build_url('files', q=query, alt='json', pageSize=1000)) + built_url = built_furl.add(self.FOLDER_FIELDS).url full_resp = [] while built_url: async with self.request( @@ -590,8 +642,8 @@ async def _folder_metadata(self, path: wb_path.WaterButlerPath, raw: bool=False) ) as resp: resp_json = await resp.json() full_resp.extend([ - self._serialize_item(path.child(item['title']), item, raw=raw) - for item in resp_json['items'] + self._serialize_file(path.child(file['name']), file, raw=raw) + for file in resp_json['files'] ]) built_url = resp_json.get('nextLink', None) return full_resp @@ -619,8 +671,7 @@ async def _file_metadata(self, ``_file_metadata.revision_is_valid``: if a revision was given, was it valid? A revision is "valid" if it doesn't end with our sentinal string (`settings.DRIVE_IGNORE_VERSION`). - ``_file_metadata.user_role``: What role did the user possess? Helps identify other roles - for which revision information isn't available. + ``_file_metadata.capabilities``: What capabilities did the user possess? :param GoogleDrivePath path: the path of the file whose metadata is being requested :param str revision: a string representing the ID of the revision (default: `None`) @@ -637,12 +688,14 @@ async def _file_metadata(self, self.metrics.add('_file_metadata.revision_is_valid', valid_revision) if revision and valid_revision: - url = self.build_url('files', path.identifier, 'revisions', revision) + meta_furl = furl.furl(self.build_url('files', path.identifier, 'revisions', revision)) + meta_url = meta_furl.add(self.REVISION_FIELDS).url else: - url = self.build_url('files', path.identifier) + meta_furl = furl.furl(self.build_url('files', path.identifier or '')) + meta_url = meta_furl.add(self.FILE_FIELDS).url async with self.request( - 'GET', url, + 'GET', meta_url, expects=(200, 403, 404, ), throws=exceptions.MetadataError, ) as resp: @@ -657,16 +710,16 @@ async def _file_metadata(self, if revision and valid_revision: return GoogleDriveFileRevisionMetadata(data, path) - user_role = data['userPermission']['role'] - self.metrics.add('_file_metadata.user_role', user_role) - can_access_revisions = user_role in self.ROLES_ALLOWING_REVISIONS + self.metrics.add('_file_metadata.user_capabilities', data['capabilities']) if drive_utils.is_docs_file(data): - if can_access_revisions: + if data.get('capabilities', {}).get('canReadRevisions', None): return await self._handle_docs_versioning(path, data, raw=raw) else: # Revisions are not available for some sharing configurations. If revisions list is # empty, use the etag of the file plus a sentinel string as a dummy revision ID. - data['version'] = data['etag'] + settings.DRIVE_IGNORE_VERSION + # Revision representation does not return md5 + etag = '{}::{}'.format(data['id'], data['modifiedTime']) + data['version'] = etag + settings.DRIVE_IGNORE_VERSION return data if raw else GoogleDriveFileMetadata(data, path) @@ -685,22 +738,22 @@ async def _delete_folder_contents(self, path: wb_path.WaterButlerPath) -> None: resp = await self.make_request( 'GET', self.build_url('files', - q="'{}' in parents".format(file_id), - fields='items(id)'), + q="'{}' in parents and trashed = false".format(file_id), + fields='files(id)'), expects=(200, ), throws=exceptions.MetadataError) try: - child_ids = (await resp.json())['items'] + child_ids = (await resp.json())['files'] except (KeyError, IndexError): raise exceptions.MetadataError('{} not found'.format(str(path)), code=HTTPStatus.NOT_FOUND) for child in child_ids: await self.make_request( - 'PUT', + 'PATCH', self.build_url('files', child['id']), - data=json.dumps({'labels': {'trashed': 'true'}}), + data=json.dumps({'trashed': 'true'}), headers={'Content-Type': 'application/json'}, expects=(200, ), throws=exceptions.DeleteError) diff --git a/waterbutler/providers/googledrive/settings.py b/waterbutler/providers/googledrive/settings.py index 4cfa1afea..21170e8cf 100644 --- a/waterbutler/providers/googledrive/settings.py +++ b/waterbutler/providers/googledrive/settings.py @@ -2,7 +2,7 @@ config = settings.child('GOOGLEDRIVE_PROVIDER_CONFIG') +BASE_URL = config.get('BASE_URL', 'https://www.googleapis.com/drive/v3') +BASE_UPLOAD_URL = config.get('BASE_UPLOAD_URL', 'https://www.googleapis.com/upload/drive/v3') -BASE_URL = config.get('BASE_URL', 'https://www.googleapis.com/drive/v2') -BASE_UPLOAD_URL = config.get('BASE_UPLOAD_URL', 'https://www.googleapis.com/upload/drive/v2') DRIVE_IGNORE_VERSION = config.get('DRIVE_IGNORE_VERSION', '0000000000000000000000000000000000000') diff --git a/waterbutler/providers/googledrive/utils.py b/waterbutler/providers/googledrive/utils.py index 6f5b1be98..8449bb079 100644 --- a/waterbutler/providers/googledrive/utils.py +++ b/waterbutler/providers/googledrive/utils.py @@ -3,62 +3,66 @@ 'mime_type': 'application/vnd.google-apps.document', 'ext': '.gdoc', 'download_ext': '.docx', - 'type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'export_mimetype': + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }, { 'mime_type': 'application/vnd.google-apps.drawing', 'ext': '.gdraw', 'download_ext': '.jpg', - 'type': 'image/jpeg', + 'export_mimetype': 'image/jpeg', }, { 'mime_type': 'application/vnd.google-apps.spreadsheet', 'ext': '.gsheet', 'download_ext': '.xlsx', - 'type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'export_mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, { 'mime_type': 'application/vnd.google-apps.presentation', 'ext': '.gslides', 'download_ext': '.pptx', - 'type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'export_mimetype': + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }, ] +DOCS_MIMES = [doc_format['mime_type'] for doc_format in DOCS_FORMATS] DOCS_DEFAULT_FORMAT = { + 'mime_type': '', 'ext': '', 'download_ext': '.pdf', - 'type': 'application/pdf', + 'export_mimetype': 'application/pdf', } def is_docs_file(metadata): - """Only Docs files have the "exportLinks" key.""" - return metadata.get('exportLinks') + return metadata['mimeType'] in DOCS_MIMES def get_mimetype_from_ext(ext): - for format in DOCS_FORMATS: - if format['ext'] == ext: - return format['mime_type'] + for doc_format in DOCS_FORMATS: + if doc_format['ext'] == ext: + return doc_format['mime_type'] + + +def get_export_mimetype_from_ext(ext): + for doc_format in DOCS_FORMATS: + if doc_format['ext'] == ext: + return doc_format['export_mimetype'] def get_format(metadata): - for format in DOCS_FORMATS: - if format['mime_type'] == metadata['mimeType']: - return format + for doc_format in DOCS_FORMATS: + if doc_format['mime_type'] == metadata['mimeType']: + return doc_format return DOCS_DEFAULT_FORMAT def get_extension(metadata): - format = get_format(metadata) - return format['ext'] + doc_format = get_format(metadata) + return doc_format['ext'] def get_download_extension(metadata): - format = get_format(metadata) - return format['download_ext'] - - -def get_export_link(metadata): - format = get_format(metadata) - return metadata['exportLinks'][format['type']] + doc_format = get_format(metadata) + return doc_format['download_ext'] From 34763f0c217e873ea3b72cc68726ea9efe025842 Mon Sep 17 00:00:00 2001 From: longze chen Date: Fri, 5 Jan 2018 12:34:14 -0500 Subject: [PATCH 2/3] Refactor style with minor code change for gdrive. --- waterbutler/providers/googledrive/metadata.py | 29 +- waterbutler/providers/googledrive/provider.py | 402 +++++++++++------- waterbutler/providers/googledrive/utils.py | 14 +- 3 files changed, 268 insertions(+), 177 deletions(-) diff --git a/waterbutler/providers/googledrive/metadata.py b/waterbutler/providers/googledrive/metadata.py index 6981cfcd1..d15cc488b 100644 --- a/waterbutler/providers/googledrive/metadata.py +++ b/waterbutler/providers/googledrive/metadata.py @@ -1,10 +1,9 @@ -from waterbutler.core import metadata -import waterbutler.core.utils as core_utils +from waterbutler.core import utils as core_utils +from waterbutler.core import metadata as core_metadata +from waterbutler.providers.googledrive import utils as provider_utils -from waterbutler.providers.googledrive import utils - -class BaseGoogleDriveMetadata(metadata.BaseMetadata): +class BaseGoogleDriveMetadata(core_metadata.BaseMetadata): def __init__(self, raw, path): super().__init__(raw) @@ -26,7 +25,7 @@ def extra(self): return {'revisionId': self.raw['version']} -class GoogleDriveFolderMetadata(BaseGoogleDriveMetadata, metadata.BaseFolderMetadata): +class GoogleDriveFolderMetadata(BaseGoogleDriveMetadata, core_metadata.BaseFolderMetadata): def __init__(self, raw, path): super().__init__(raw, path) @@ -53,7 +52,7 @@ def export_name(self): return self.name -class GoogleDriveFileMetadata(BaseGoogleDriveMetadata, metadata.BaseFileMetadata): +class GoogleDriveFileMetadata(BaseGoogleDriveMetadata, core_metadata.BaseFileMetadata): """The metadata for a single file on Google Drive. This class expects a the ``raw`` property to be the response[1] from the GDrive v3 file metadata endpoint[2]. @@ -69,7 +68,7 @@ def id(self): def name(self): name = self._file_name if self.is_google_doc: - ext = utils.get_extension(self.raw) + ext = provider_utils.get_extension(self.raw) name += ext return name @@ -77,7 +76,7 @@ def name(self): def path(self): path = '/' + self._path.raw_path if self.is_google_doc: - ext = utils.get_extension(self.raw) + ext = provider_utils.get_extension(self.raw) path += ext return path @@ -85,7 +84,7 @@ def path(self): def materialized_path(self): materialized = str(self._path) if self.is_google_doc: - ext = utils.get_extension(self.raw) + ext = provider_utils.get_extension(self.raw) materialized += ext return materialized @@ -116,7 +115,7 @@ def extra(self): ret['webView'] = self.raw.get('webViewLink') if self.is_google_doc: - ret['downloadExt'] = utils.get_download_extension(self.raw) + ret['downloadExt'] = provider_utils.get_download_extension(self.raw) else: if not hasattr(ret, 'hashes'): ret['hashes'] = {} @@ -126,13 +125,13 @@ def extra(self): @property def is_google_doc(self): - return utils.is_docs_file(self.raw) + return provider_utils.is_docs_file(self.raw) @property def export_name(self): name = self._file_name if self.is_google_doc: - ext = utils.get_download_extension(self.raw) + ext = provider_utils.get_download_extension(self.raw) name += ext return name @@ -168,7 +167,7 @@ def extra(self): appropriate. GDocs don't have an md5, non-GDocs don't need a downloadExt. """ if self.is_google_doc: - return {'downloadExt': utils.get_download_extension(self.raw)} + return {'downloadExt': provider_utils.get_download_extension(self.raw)} return {'md5': self.raw['md5Checksum']} @property @@ -176,7 +175,7 @@ def _file_name(self): return self.raw.get('originalFilename', self._path.name) -class GoogleDriveRevision(metadata.BaseFileRevisionMetadata): +class GoogleDriveRevision(core_metadata.BaseFileRevisionMetadata): @property def version_identifier(self): diff --git a/waterbutler/providers/googledrive/provider.py b/waterbutler/providers/googledrive/provider.py index a5f90094e..1f6e2bf3e 100644 --- a/waterbutler/providers/googledrive/provider.py +++ b/waterbutler/providers/googledrive/provider.py @@ -8,18 +8,17 @@ import furl -from waterbutler.core import streams -from waterbutler.core import provider -from waterbutler.core import exceptions -from waterbutler.core import path as wb_path - -from waterbutler.providers.googledrive import settings -from waterbutler.providers.googledrive import utils as drive_utils -from waterbutler.providers.googledrive.metadata import (BaseGoogleDriveMetadata, +from waterbutler.core import exceptions, streams +from waterbutler.core.provider import BaseProvider, build_url +from waterbutler.core.path import WaterButlerPath, WaterButlerPathPart + +from waterbutler.providers.googledrive import utils as provider_utils +from waterbutler.providers.googledrive import settings as provider_settings +from waterbutler.providers.googledrive.metadata import (GoogleDriveRevision, + BaseGoogleDriveMetadata, GoogleDriveFileMetadata, - GoogleDriveFileRevisionMetadata, GoogleDriveFolderMetadata, - GoogleDriveRevision) + GoogleDriveFileRevisionMetadata) def clean_query(query: str) -> str: @@ -28,17 +27,16 @@ def clean_query(query: str) -> str: return query.replace('\\', r'\\').replace("'", r"\'") -class GoogleDrivePathPart(wb_path.WaterButlerPathPart): +class GoogleDrivePathPart(WaterButlerPathPart): DECODE = parse.unquote - # TODO: mypy lacks a syntax to define kwargs for callables - ENCODE = functools.partial(parse.quote, safe='') # type: ignore + ENCODE = functools.partial(parse.quote, safe='') -class GoogleDrivePath(wb_path.WaterButlerPath): +class GoogleDrivePath(WaterButlerPath): PART_CLASS = GoogleDrivePathPart -class GoogleDriveProvider(provider.BaseProvider): +class GoogleDriveProvider(BaseProvider): """Provider for Google's Drive cloud storage service. This provider uses the v3 Drive API. @@ -50,6 +48,9 @@ class GoogleDriveProvider(provider.BaseProvider): * Google doc files (``.gdoc``, ``.gsheet``, ``.gsheet``, ``.gdraw``) cannot be downloaded in their native format and must be exported to another format. e.g. ``.gdoc`` to ``.docx`` + * We use "Google Docs", "Docs" and "Docs File" interchangeably to denote Google doc files. + Other files are usually mentioned as GDrive files or GoogleDrive files. + * Some Google doc files (currently ``.gform`` and ``.gmap``) do not have an available export format and cannot be downloaded at all. @@ -71,17 +72,32 @@ class GoogleDriveProvider(provider.BaseProvider): or download request for a readonly file with a revision value that doesn't end with the sentinel value will always return a 404 Not Found. """ + NAME = 'googledrive' - BASE_URL = settings.BASE_URL + BASE_URL = provider_settings.BASE_URL FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder' - FILE_FIELDS = {'fields': 'version, id, name, size, modifiedTime, createdTime, mimeType, \ - webViewLink, webContentLink, md5Checksum, capabilities(canDelete, canEdit, \ - canTrash, canDownload, canRename, canReadRevisions, canShare, canCopy)'} - FOLDER_FIELDS = {'fields': 'files({})'.format(FILE_FIELDS['fields'])} - REVISION_FIELDS = {'fields': 'id, originalFilename, size, modifiedTime, mimeType, md5Checksum'} - # https://developers.google.com/drive/v3/web/about-permissions#roles - # 'reader' and 'commenter' are not authorized to access the revisions list + # Access to files & folders is determined by an access control list (ACL). An ACL is a list + # of permissions that determine whether or not users can perform actions on a file such as + # read or write: https://developers.google.com/drive/v3/web/about-permissions. + # + # Each permission in the Google Drive API has a role. A role defines what users can do with + # a file: https://developers.google.com/drive/v3/web/about-permissions#roles. Please note + # that 'reader' and 'commenter' are not authorized to access the revisions list. + CAP_FIELDS = { + 'fields': 'canDelete, canEdit, canTrash, canDownload, canRename, canReadRevisions, ' + 'canShare, canCopy' + } + FILE_FIELDS = { + 'fields': 'version, id, name, size, modifiedTime, createdTime, mimeType, webViewLink, ' + 'webContentLink, md5Checksum, capabilities({})'.format(CAP_FIELDS['fields']) + } + FOLDER_FIELDS = { + 'fields': 'files({})'.format(FILE_FIELDS['fields']) + } + REVISION_FIELDS = { + 'fields': 'id, originalFilename, size, modifiedTime, mimeType, md5Checksum' + } def __init__(self, auth: dict, credentials: dict, settings: dict) -> None: super().__init__(auth, credentials, settings) @@ -109,25 +125,29 @@ async def validate_path(self, path: str, **kwargs) -> GoogleDrivePath: names, ids = zip(*[(parse.quote(x['name'], safe=''), x['id']) for x in parts]) return GoogleDrivePath('/'.join(names), _ids=ids, folder='folder' in parts[-1]['mimeType']) - async def revalidate_path(self, - base: wb_path.WaterButlerPath, - name: str, - folder: bool = None) -> wb_path.WaterButlerPath: - # TODO Redo the logic here folders names ending in /s - # Will probably break + async def revalidate_path( + self, + base: WaterButlerPath, + name: str, + folder: bool = None + ) -> WaterButlerPath: + + # TODO: Redo the logic here. Folder with names ending in /s will probably break if '/' in name.lstrip('/') and '%' not in name: - # DAZ and MnC may pass unquoted names which break - # if the name contains a / in it + # DAZ and MnC may pass unquoted names which break if the name contains a / name = parse.quote(name.lstrip('/'), safe='') if not name.endswith('/') and folder: name += '/' - parts = await self._resolve_path_to_ids(name, start_at=[{ - 'name': base.name, - 'mimeType': 'folder', - 'id': base.identifier, - }]) + parts = await self._resolve_path_to_ids( + name, + start_at=[{ + 'name': base.name, + 'mimeType': 'folder', + 'id': base.identifier, + }] + ) _id, name, mime = list(map(parts[-1].__getitem__, ('id', 'name', 'mimeType'))) return base.child(name, _id=_id, folder='folder' in mime) @@ -138,40 +158,47 @@ def default_headers(self) -> dict: def can_duplicate_names(self) -> bool: return True - def can_intra_move(self, - other: provider.BaseProvider, - path=None) -> bool: + def can_intra_move( + self, + other: BaseProvider, + path=None + ) -> bool: return self == other - def can_intra_copy(self, - other: provider.BaseProvider, - path=None) -> bool: - # gdrive doesn't support intra-copy on folders + def can_intra_copy( + self, + other: BaseProvider, + path=None + ) -> bool: + # GoogleDrive doesn't support intra-copy on folders return self == other and (path and path.is_file) - async def intra_move(self, # type: ignore - dest_provider: provider.BaseProvider, - src_path: wb_path.WaterButlerPath, - dest_path: wb_path.WaterButlerPath) \ - -> typing.Tuple[BaseGoogleDriveMetadata, bool]: + async def intra_move( + self, + dest_provider: BaseProvider, + src_path: WaterButlerPath, + dest_path: WaterButlerPath + ) -> typing.Tuple[BaseGoogleDriveMetadata, bool]: + self.metrics.add('intra_move.destination_exists', dest_path.identifier is not None) if dest_path.identifier: await dest_provider.delete(dest_path) - request_furl = furl.furl(self.build_url('files', src_path.identifier, - removeParents=src_path.parent.identifier, - addParents=dest_path.parent.identifier)) + request_furl = furl.furl( + self.build_url( + 'files', + src_path.identifier, + removeParents=src_path.parent.identifier, + addParents=dest_path.parent.identifier + ) + ) request_url = request_furl.add(self.FILE_FIELDS).url async with self.request( 'PATCH', request_url, - headers={ - 'Content-Type': 'application/json' - }, - data=json.dumps({ - 'name': dest_path.name - }), + headers={'Content-Type': 'application/json'}, + data=json.dumps({'name': dest_path.name}), expects=(200, ), throws=exceptions.IntraMoveError, ) as resp: @@ -185,13 +212,15 @@ async def intra_move(self, # type: ignore metadata._children = await self._folder_metadata(dest_path) return metadata, created else: - return GoogleDriveFileMetadata(data, dest_path), created # type: ignore + return GoogleDriveFileMetadata(data, dest_path), created + + async def intra_copy( + self, + dest_provider: BaseProvider, + src_path: WaterButlerPath, + dest_path: WaterButlerPath + ) -> typing.Tuple[GoogleDriveFileMetadata, bool]: - async def intra_copy(self, - dest_provider: provider.BaseProvider, - src_path: wb_path.WaterButlerPath, - dest_path: wb_path.WaterButlerPath) \ - -> typing.Tuple[GoogleDriveFileMetadata, bool]: self.metrics.add('intra_copy.destination_exists', dest_path.identifier is not None) if dest_path.identifier: await dest_provider.delete(dest_path) @@ -214,13 +243,16 @@ async def intra_copy(self, # GoogleDrive doesn't support intra-copy for folders, so dest_path will always # be a file. See can_intra_copy() for type check. + assert dest_path.is_file, 'Google Drive does not support intra-copy for folders' return GoogleDriveFileMetadata(data, dest_path), dest_path.identifier is None - async def download(self, # type: ignore - path: GoogleDrivePath, - revision: str=None, - range: typing.Tuple[int, int]=None, - **kwargs) -> streams.BaseStream: # type: ignore + async def download( + self, + path: GoogleDrivePath, + revision: str=None, + range: typing.Tuple[int, int]=None, + **kwargs + ) -> streams.BaseStream: """Download the file at `path`. If `revision` is present, attempt to download that revision of the file. See **Revisions** in the class doctring for an explanation of this provider's revision handling. The actual revision handling is done in `_file_metadata()`. @@ -239,12 +271,13 @@ async def download(self, # type: ignore """ metadata = await self.metadata(path) - if revision and revision.endswith(settings.DRIVE_IGNORE_VERSION): + if revision and revision.endswith(provider_settings.DRIVE_IGNORE_VERSION): revision = None - is_docs_file = drive_utils.is_docs_file(metadata.raw) - # GoogleDrive v3 API does not allow the download of GoogleDoc revisions - # Use v2 API for this functionality + is_docs_file = provider_utils.is_docs_file(metadata.raw) + + # Is a revision and is a Docs file if revision and is_docs_file: + # v3 API does not allow the download of Docs file revisions, use v2 API instead meta_url = self.build_url('files', path.identifier, 'revisions', revision) meta_url = meta_url.replace('/v3/', '/v2/', 1) @@ -254,28 +287,34 @@ async def download(self, # type: ignore throws=exceptions.MetadataError, ) as resp: try: + # aiohttp.ClientResponse calls json.loads() on ._content when .json() + # is called, which throws built-in TypeError or json.JSONDecodeError data = await resp.json() - except: # some 404s return a string instead of json + except (TypeError, json.JSONDecodeError): + # some 404s return a string instead of json data = await resp.read() if resp.status != 200: raise exceptions.NotFoundError(path) - format = drive_utils.get_format(data) - url = data['exportLinks'][format['export_mimetype']] + docs_format = provider_utils.get_docs_format(data) + url = data['exportLinks'][docs_format['export_mimetype']] # Is a revision but not a Docs file elif revision: - url = self.build_url('files', metadata.raw.get('id'), - 'revisions', revision, alt='media') + url = self.build_url( + 'files', + metadata.raw.get('id'), + 'revisions', + revision, alt='media' + ) # Is neither a revision nor a Docs file elif not is_docs_file: - # webContentLink is only available for files with binary content. - # e.g. Not GoogleDocs + # webContentLink is only available for files with binary content. e.g. Not GoogleDocs url = metadata.raw.get('webContentLink') # Is a Docs file but not a revision else: ext = os.path.splitext(metadata.name)[-1] - mime_type = drive_utils.get_export_mimetype_from_ext(ext) + mime_type = provider_utils.get_export_mimetype_from_ext(ext) url = self.build_url('files', metadata.raw.get('id'), 'export', mimeType=mime_type) download_resp = await self.make_request( @@ -289,17 +328,23 @@ async def download(self, # type: ignore if metadata.size is not None: # type: ignore return streams.ResponseStreamReader(download_resp, size=metadata.size) # type: ignore - # google docs, not drive files, have no way to get the file size - # must buffer the entire file into memory + # As mentioned, Google Docs don't have a size until they're exported. + # Waterbutler must download them, then re-stream them as a StringStream. stream = streams.StringStream(await download_resp.read()) if download_resp.headers.get('Content-Type'): # TODO: Add these properties to base class officially, instead of as one-off - stream.content_type = download_resp.headers['Content-Type'] # type: ignore - stream.name = metadata.export_name # type: ignore + stream.content_type = download_resp.headers['Content-Type'] + stream.name = metadata.export_name return stream - async def upload(self, stream, path: wb_path.WaterButlerPath, *args, **kwargs) \ - -> typing.Tuple[GoogleDriveFileMetadata, bool]: + async def upload( + self, + stream, + path: WaterButlerPath, + *args, + **kwargs + ) -> typing.Tuple[GoogleDriveFileMetadata, bool]: + assert path.is_file if path.identifier: @@ -310,8 +355,12 @@ async def upload(self, stream, path: wb_path.WaterButlerPath, *args, **kwargs) \ stream.add_writer('md5', streams.HashStreamWriter(hashlib.md5)) upload_metadata = self._build_upload_metadata(path.parent.identifier, path.name) - upload_id = await self._start_resumable_upload(not path.identifier, segments, stream.size, - upload_metadata) + upload_id = await self._start_resumable_upload( + not path.identifier, + segments, + stream.size, + upload_metadata + ) data = await self._finish_resumable_upload(segments, stream, upload_id) if data['md5Checksum'] != stream.writers['md5'].hexdigest: @@ -319,12 +368,15 @@ async def upload(self, stream, path: wb_path.WaterButlerPath, *args, **kwargs) \ return GoogleDriveFileMetadata(data, path), path.identifier is None - async def delete(self, # type: ignore - path: GoogleDrivePath, - confirm_delete: int=0, - **kwargs) -> None: + async def delete( + self, + path: GoogleDrivePath, + confirm_delete: int=0, + **kwargs + ) -> None: """Given a WaterButlerPath, delete that path - :param GoogleDrivePath: Path to be deleted + + :param GoogleDrivePath path: Path to be deleted :param int confirm_delete: Must be 1 to confirm root folder delete :rtype: None :raises: :class:`waterbutler.core.exceptions.NotFoundError` @@ -335,6 +387,7 @@ async def delete(self, # type: ignore the contents of provider root path will be deleted. But not the provider root itself. """ + if not path.identifier: raise exceptions.NotFoundError(str(path)) @@ -371,11 +424,14 @@ def _build_query(self, folder_id: str, name: str=None) -> str: queries.append("name = '{}'".format(clean_query(name))) return ' and '.join(queries) - async def metadata(self, # type: ignore - path: GoogleDrivePath, - raw: bool = False, - revision=None, - **kwargs) -> typing.Union[dict, BaseGoogleDriveMetadata, typing.List[typing.Union[BaseGoogleDriveMetadata, dict]]]: + async def metadata( + self, + path: GoogleDrivePath, + raw: bool=False, + revision=None, + **kwargs + ) -> typing.Union[dict, BaseGoogleDriveMetadata, typing.List[typing.Union[BaseGoogleDriveMetadata, dict]]]: + if path.identifier is None: raise exceptions.MetadataError('{} not found'.format(str(path)), code=404) @@ -384,11 +440,15 @@ async def metadata(self, # type: ignore return await self._file_metadata(path, revision=revision, raw=raw) - async def revisions(self, path: GoogleDrivePath, **kwargs) -> typing.List[GoogleDriveRevision]: # type: ignore + async def revisions( + self, + path: GoogleDrivePath, + **kwargs + ) -> typing.List[GoogleDriveRevision]: """Returns list of revisions for the file at ``path``. Google Drive will not allow a user to view the revision list of a file if they only have - view or commenting permissions. It will return a 403 Unathorized. If that happens, then + view or commenting permissions. It will return a 403 Unauthorized. If that happens, then we construct a recognizable dummy revision based off of the metadata of the current file version. @@ -399,6 +459,7 @@ async def revisions(self, path: GoogleDrivePath, **kwargs) -> typing.List[Google :rtype: `list(GoogleDriveRevision)` :return: list of `GoogleDriveRevision` objects representing revisions of the file """ + if path.identifier is None: raise exceptions.NotFoundError(str(path)) @@ -419,18 +480,24 @@ async def revisions(self, path: GoogleDrivePath, **kwargs) -> typing.List[Google # Use dummy ID if no revisions found metadata = await self.metadata(path, raw=True) - # GoogleDrive does not return etag for GoogleDocs - etag = metadata.get('etag', None) or '{}::{}'.format(metadata['id'], - metadata['modifiedTime']) + # TODO: considering keep using v2 to obtain etag + # GoogleDrive v3 does not return etag for GoogleDocs. Current alternative is + # to use metadata['id'] and metadata['modifiedTime'] together + alt_etag = '{}::{}'.format(metadata['id'], metadata['modifiedTime']) + etag = metadata.get('etag', None) or alt_etag + return [GoogleDriveRevision({ - 'modifiedTime': metadata['modifiedTime'], # type: ignore - 'id': etag + settings.DRIVE_IGNORE_VERSION, + 'modifiedTime': metadata['modifiedTime'], + 'id': etag + provider_settings.DRIVE_IGNORE_VERSION, })] - async def create_folder(self, - path: wb_path.WaterButlerPath, - folder_precheck: bool=True, - **kwargs) -> GoogleDriveFolderMetadata: + async def create_folder( + self, + path: WaterButlerPath, + folder_precheck: bool=True, + **kwargs + ) -> GoogleDriveFolderMetadata: + GoogleDrivePath.validate_folder(path) if folder_precheck: @@ -442,16 +509,14 @@ async def create_folder(self, async with self.request( 'POST', request_url, - headers={ - 'Content-Type': 'application/json', - }, + headers={'Content-Type': 'application/json'}, data=json.dumps({ 'name': path.name, 'parents': [path.parent.identifier], 'mimeType': self.FOLDER_MIME_TYPE, }), expects=(200, ), - throws=exceptions.CreateFolderError, + throws=exceptions.CreateFolderError ) as resp: return GoogleDriveFolderMetadata(await resp.json(), path) @@ -460,12 +525,15 @@ def path_from_metadata(self, parent_path, metadata): return parent_path.child(metadata.export_name, _id=metadata.id, folder=metadata.is_folder) def _build_upload_url(self, *segments, **query): - return provider.build_url(settings.BASE_UPLOAD_URL, *segments, **query) - - def _serialize_file(self, - path: wb_path.WaterButlerPath, - file: dict, - raw: bool=False) -> typing.Union[BaseGoogleDriveMetadata, dict]: + """ Upload URL must be built with the `BASE_UPLOAD_URL`, do not use `self.build_url()` """ + return build_url(provider_settings.BASE_UPLOAD_URL, *segments, **query) + + def _serialize_file( + self, + path: WaterButlerPath, + file: dict, + raw: bool=False + ) -> typing.Union[BaseGoogleDriveMetadata, dict]: if raw: return file if file['mimeType'] == self.FOLDER_MIME_TYPE: @@ -473,23 +541,24 @@ def _serialize_file(self, return GoogleDriveFileMetadata(file, path) def _build_upload_metadata(self, folder_id: str, name: str) -> dict: - return { - 'parents': [folder_id], - 'name': name, - } + return {'parents': [folder_id], 'name': name} - async def _start_resumable_upload(self, - created: bool, - segments: typing.Sequence[str], - size, - metadata: dict) -> str: + async def _start_resumable_upload( + self, + created: bool, + segments: typing.Sequence[str], + size: int, + metadata: dict + ) -> str: if not created: del metadata['parents'] - request_furl = furl.furl(self._build_upload_url('files', *segments, - uploadType='resumable')) + request_furl = furl.furl( + self._build_upload_url('files', *segments, uploadType='resumable') + ) request_url = request_furl.add(self.FILE_FIELDS).url + async with self.request( 'POST' if created else 'PATCH', request_url, @@ -502,6 +571,7 @@ async def _start_resumable_upload(self, throws=exceptions.UploadError, ) as resp: location = furl.furl(resp.headers['LOCATION']) + return location.args['upload_id'] async def _finish_resumable_upload(self, segments: typing.Sequence[str], stream, upload_id): @@ -520,6 +590,7 @@ async def _resolve_path_to_ids(self, path, start_at=None): something that matches ``path``. Returns a list of dicts for each part of the path, with ``name``, ``mimeType``, and ``id`` keys. """ + self.metrics.incr('called_resolve_path_to_ids') ret = start_at or [{ 'name': '', @@ -537,7 +608,7 @@ async def _resolve_path_to_ids(self, path, start_at=None): part_name, part_is_folder = current_part[0], current_part[1] name, ext = os.path.splitext(part_name) if not part_is_folder and ext in ('.gdoc', '.gdraw', '.gslides', '.gsheet'): - gd_mimetype = drive_utils.get_mimetype_from_ext(ext) + gd_mimetype = provider_utils.get_mimetype_from_ext(ext) query = "name = '{}' " \ "and trashed = false " \ "and '{}' in parents " \ @@ -623,16 +694,21 @@ async def _handle_docs_versioning(self, path: GoogleDrivePath, file: dict, raw: file['version'] = revisions_data['revisions'][-1]['id'] else: # If there are no revisions use etag as vid - file['version'] = file['etag'] + settings.DRIVE_IGNORE_VERSION + file['version'] = file['etag'] + provider_settings.DRIVE_IGNORE_VERSION return self._serialize_file(path, file, raw=raw) - async def _folder_metadata(self, path: wb_path.WaterButlerPath, raw: bool=False) \ - -> typing.List[typing.Union[BaseGoogleDriveMetadata, dict]]: + async def _folder_metadata( + self, + path: WaterButlerPath, + raw: bool=False + ) -> typing.List[typing.Union[BaseGoogleDriveMetadata, dict]]: + query = self._build_query(path.identifier) built_furl = furl.furl(self.build_url('files', q=query, alt='json', pageSize=1000)) built_url = built_furl.add(self.FOLDER_FIELDS).url full_resp = [] + while built_url: async with self.request( 'GET', @@ -648,10 +724,12 @@ async def _folder_metadata(self, path: wb_path.WaterButlerPath, raw: bool=False) built_url = resp_json.get('nextLink', None) return full_resp - async def _file_metadata(self, - path: GoogleDrivePath, - revision: str=None, - raw: bool=False): + async def _file_metadata( + self, + path: GoogleDrivePath, + revision: str=None, + raw: bool=False + ): """ Returns metadata for the file identified by `path`. If the `revision` arg is set, will attempt to return metadata for the given revision of the file. If the revision does not exist, ``_file_metadata`` will throw a 404. @@ -669,7 +747,7 @@ async def _file_metadata(self, ``_file_metadata.got_revision``: did this request include a revision parameter? ``_file_metadata.revision_is_valid``: if a revision was given, was it valid? A revision is - "valid" if it doesn't end with our sentinal string (`settings.DRIVE_IGNORE_VERSION`). + "valid" if it doesn't end with our sentinel string (`settings.DRIVE_IGNORE_VERSION`). ``_file_metadata.capabilities``: What capabilities did the user possess? @@ -678,12 +756,12 @@ async def _file_metadata(self, :param bool raw: should we return the raw response object from the GDrive API? :rtype: GoogleDriveFileMetadata :rtype: dict - :return: a metadata for the googledoc or the raw response object from the GDrive API + :return: a metadata for the GoogleDoc or the raw response object from the GDrive API """ self.metrics.add('_file_metadata.got_revision', revision is not None) - valid_revision = revision and not revision.endswith(settings.DRIVE_IGNORE_VERSION) + valid_revision = revision and not revision.endswith(provider_settings.DRIVE_IGNORE_VERSION) if revision: self.metrics.add('_file_metadata.revision_is_valid', valid_revision) @@ -700,8 +778,11 @@ async def _file_metadata(self, throws=exceptions.MetadataError, ) as resp: try: + # aiohttp.ClientResponse calls json.loads() on ._content when .json() + # is called, which throws built-in TypeError or json.JSONDecodeError data = await resp.json() - except: # some 404s return a string instead of json + except (TypeError, json.JSONDecodeError): + # some 404s return a string instead of json data = await resp.read() if resp.status != 200: @@ -711,20 +792,21 @@ async def _file_metadata(self, return GoogleDriveFileRevisionMetadata(data, path) self.metrics.add('_file_metadata.user_capabilities', data['capabilities']) - if drive_utils.is_docs_file(data): + if provider_utils.is_docs_file(data): if data.get('capabilities', {}).get('canReadRevisions', None): return await self._handle_docs_versioning(path, data, raw=raw) else: # Revisions are not available for some sharing configurations. If revisions list is - # empty, use the etag of the file plus a sentinel string as a dummy revision ID. - # Revision representation does not return md5 - etag = '{}::{}'.format(data['id'], data['modifiedTime']) - data['version'] = etag + settings.DRIVE_IGNORE_VERSION + # empty, use the etag (v2) or its alternative (v3) of the file plus a sentinel + # string as a dummy revision ID. Revision representation does not return md5. + alt_etag = '{}::{}'.format(data['id'], data['modifiedTime']) + etag = data.get('etag', None) or alt_etag + data['version'] = etag + provider_settings.DRIVE_IGNORE_VERSION return data if raw else GoogleDriveFileMetadata(data, path) - async def _delete_folder_contents(self, path: wb_path.WaterButlerPath) -> None: - """Given a WaterButlerPath, delete all contents of folder + async def _delete_folder_contents(self, path: WaterButlerPath) -> None: + """Given a WaterButlerPath, delete all contents of the folder :param WaterButlerPath path: Folder to be emptied :rtype: None @@ -732,22 +814,29 @@ async def _delete_folder_contents(self, path: wb_path.WaterButlerPath) -> None: :raises: :class:`waterbutler.core.exceptions.MetadataError` :raises: :class:`waterbutler.core.exceptions.DeleteError` """ + file_id = path.identifier if not file_id: raise exceptions.NotFoundError(str(path)) + resp = await self.make_request( 'GET', - self.build_url('files', - q="'{}' in parents and trashed = false".format(file_id), - fields='files(id)'), + self.build_url( + 'files', + q="'{}' in parents and trashed = false".format(file_id), + fields='files(id)' + ), expects=(200, ), - throws=exceptions.MetadataError) + throws=exceptions.MetadataError + ) try: child_ids = (await resp.json())['files'] except (KeyError, IndexError): - raise exceptions.MetadataError('{} not found'.format(str(path)), - code=HTTPStatus.NOT_FOUND) + raise exceptions.MetadataError( + '{} not found'.format(str(path)), + code=HTTPStatus.NOT_FOUND + ) for child in child_ids: await self.make_request( @@ -756,4 +845,5 @@ async def _delete_folder_contents(self, path: wb_path.WaterButlerPath) -> None: data=json.dumps({'trashed': 'true'}), headers={'Content-Type': 'application/json'}, expects=(200, ), - throws=exceptions.DeleteError) + throws=exceptions.DeleteError + ) diff --git a/waterbutler/providers/googledrive/utils.py b/waterbutler/providers/googledrive/utils.py index 8449bb079..3b42baf83 100644 --- a/waterbutler/providers/googledrive/utils.py +++ b/waterbutler/providers/googledrive/utils.py @@ -26,7 +26,9 @@ 'application/vnd.openxmlformats-officedocument.presentationml.presentation', }, ] + DOCS_MIMES = [doc_format['mime_type'] for doc_format in DOCS_FORMATS] + DOCS_DEFAULT_FORMAT = { 'mime_type': '', 'ext': '', @@ -51,18 +53,18 @@ def get_export_mimetype_from_ext(ext): return doc_format['export_mimetype'] -def get_format(metadata): - for doc_format in DOCS_FORMATS: - if doc_format['mime_type'] == metadata['mimeType']: - return doc_format +def get_docs_format(metadata): + for docs_format in DOCS_FORMATS: + if docs_format['mime_type'] == metadata['mimeType']: + return docs_format return DOCS_DEFAULT_FORMAT def get_extension(metadata): - doc_format = get_format(metadata) + doc_format = get_docs_format(metadata) return doc_format['ext'] def get_download_extension(metadata): - doc_format = get_format(metadata) + doc_format = get_docs_format(metadata) return doc_format['download_ext'] From 9266287197da9195f987b21f6bee5afd6f619c17 Mon Sep 17 00:00:00 2001 From: longze chen Date: Mon, 8 Jan 2018 14:55:15 -0500 Subject: [PATCH 3/3] Update style with trivial code change for tests: - gdrive metadata and provider tests --- tests/providers/googledrive/test_metadata.py | 20 +- tests/providers/googledrive/test_provider.py | 688 +++++++++++++------ 2 files changed, 468 insertions(+), 240 deletions(-) diff --git a/tests/providers/googledrive/test_metadata.py b/tests/providers/googledrive/test_metadata.py index 2ede6f46e..d9aff1a6e 100644 --- a/tests/providers/googledrive/test_metadata.py +++ b/tests/providers/googledrive/test_metadata.py @@ -1,19 +1,14 @@ -import pytest - import os +import pytest + from waterbutler.providers.googledrive.provider import GoogleDrivePath -from waterbutler.providers.googledrive.provider import GoogleDrivePathPart -from waterbutler.providers.googledrive.metadata import GoogleDriveRevision -from waterbutler.providers.googledrive.metadata import GoogleDriveFileMetadata -from waterbutler.providers.googledrive.metadata import GoogleDriveFolderMetadata +from waterbutler.providers.googledrive.metadata import (GoogleDriveRevision, + GoogleDriveFileMetadata, + GoogleDriveFolderMetadata) -from tests.providers.googledrive.fixtures import( - error_fixtures, - root_provider_fixtures, - revision_fixtures, - sharing_fixtures, -) +from tests.providers.googledrive.fixtures import (error_fixtures, root_provider_fixtures, + revision_fixtures, sharing_fixtures) @pytest.fixture @@ -110,6 +105,7 @@ def test_folder_metadata_slash(self, root_provider_fixtures): def test_revision_metadata(self, revision_fixtures): revision = revision_fixtures['revision_metadata'] parsed = GoogleDriveRevision(revision) + assert parsed.version_identifier == 'revision' assert parsed.version == revision['id'] assert parsed.modified == revision['modifiedTime'] diff --git a/tests/providers/googledrive/test_provider.py b/tests/providers/googledrive/test_provider.py index 4d058ce8d..5d63d4afa 100644 --- a/tests/providers/googledrive/test_provider.py +++ b/tests/providers/googledrive/test_provider.py @@ -2,30 +2,27 @@ import os import copy import json -from http import client from urllib import parse +from http import HTTPStatus import furl import pytest import aiohttpretty -from waterbutler.core import streams -from waterbutler.core import exceptions +from waterbutler.core import streams, exceptions from waterbutler.core.path import WaterButlerPath -from waterbutler.providers.googledrive import settings as ds from waterbutler.providers.googledrive import GoogleDriveProvider -from waterbutler.providers.googledrive import utils as drive_utils +from waterbutler.providers.googledrive import utils as provider_utils +from waterbutler.providers.googledrive import settings as provider_settings from waterbutler.providers.googledrive.provider import GoogleDrivePath from waterbutler.providers.googledrive.metadata import (GoogleDriveRevision, GoogleDriveFileMetadata, GoogleDriveFolderMetadata, GoogleDriveFileRevisionMetadata) -from tests.providers.googledrive.fixtures import(error_fixtures, - sharing_fixtures, - revision_fixtures, - root_provider_fixtures) +from tests.providers.googledrive.fixtures import(error_fixtures, sharing_fixtures, + revision_fixtures, root_provider_fixtures) @pytest.fixture @@ -132,8 +129,8 @@ def actual_folder_response(): def make_unauthorized_file_access_error(file_id): - message = ('The authenticated user does not have the required access ' - 'to the file {}'.format(file_id)) + message = 'The authenticated user does not have the required access to the file ' \ + '{}'.format(file_id) return json.dumps({ "error": { "errors": [ @@ -146,7 +143,7 @@ def make_unauthorized_file_access_error(file_id): } ], "message": message, - "code": 403 + "code": HTTPStatus.FORBIDDEN } }) @@ -158,7 +155,6 @@ def make_no_such_revision_error(revision_id): "errors": [ { "reason": "notFound", - "locationType": "other", "message": message, "locationType": "parameter", "location": "revisionId", @@ -166,7 +162,7 @@ def make_no_such_revision_error(revision_id): } ], "message": message, - "code": 404 + "code": HTTPStatus.NOT_FOUND } }) @@ -207,8 +203,12 @@ class TestValidatePath: @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_validate_v1_path_file(self, provider, search_for_file_response, - actual_file_response, no_folder_response): + async def test_validate_v1_path_file( + self, + provider, + search_for_file_response, + actual_file_response, no_folder_response + ): file_name = 'file.txt' file_id = '1234ideclarethumbwar' @@ -231,12 +231,13 @@ async def test_validate_v1_path_file(self, provider, search_for_file_response, try: wb_path_v1 = await provider.validate_v1_path('/' + file_name) except Exception as exc: + wb_path_v1 = None pytest.fail(str(exc)) with pytest.raises(exceptions.NotFoundError) as exc: await provider.validate_v1_path('/' + file_name + '/') - assert exc.value.code == client.NOT_FOUND + assert exc.value.code == HTTPStatus.NOT_FOUND wb_path_v0 = await provider.validate_path('/' + file_name) @@ -244,8 +245,13 @@ async def test_validate_v1_path_file(self, provider, search_for_file_response, @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_validate_v1_path_folder(self, provider, search_for_folder_response, - actual_folder_response, no_file_response): + async def test_validate_v1_path_folder( + self, + provider, + search_for_folder_response, + actual_folder_response, + no_file_response + ): folder_name = 'foofolder' folder_id = 'whyis6afraidof7' @@ -268,12 +274,13 @@ async def test_validate_v1_path_folder(self, provider, search_for_folder_respons try: wb_path_v1 = await provider.validate_v1_path('/' + folder_name + '/') except Exception as exc: + wb_path_v1 = None pytest.fail(str(exc)) with pytest.raises(exceptions.NotFoundError) as exc: await provider.validate_v1_path('/' + folder_name) - assert exc.value.code == client.NOT_FOUND + assert exc.value.code == HTTPStatus.NOT_FOUND wb_path_v0 = await provider.validate_path('/' + folder_name + '/') @@ -302,15 +309,17 @@ async def test_revalidate_path_file(self, provider, root_provider_fixtures): current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] - name, ext = os.path.splitext(part_name) query = _build_name_search_query(provider, file_name.strip('/'), file_id, False) url = provider.build_url('files', q=query, fields='files(id)') aiohttpretty.register_json_uri('GET', url, body=revalidate_path_metadata) url = provider.build_url('files', file_id, fields='id, name, mimeType') - aiohttpretty.register_json_uri('GET', url, - body=root_provider_fixtures['revalidate_path_file_metadata_2']) + aiohttpretty.register_json_uri( + 'GET', + url, + body=root_provider_fixtures['revalidate_path_file_metadata_2'] + ) result = await provider.revalidate_path(path, file_name) @@ -329,19 +338,26 @@ async def test_revalidate_path_file_gdoc(self, provider, root_provider_fixtures) current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] name, ext = os.path.splitext(part_name) - gd_ext = drive_utils.get_mimetype_from_ext(ext) - query = "name = '{}' " \ - "and trashed = false " \ - "and '{}' in parents " \ - "and mimeType = '{}'".format(clean_query(name), file_id, gd_ext) + gd_ext = provider_utils.get_mimetype_from_ext(ext) + query = "name = '{}' and trashed = false and '{}' in parents and mimeType = '{}'".format( + clean_query(name), + file_id, + gd_ext + ) url = provider.build_url('files', q=query, fields='files(id)') - aiohttpretty.register_json_uri('GET', url, - body=root_provider_fixtures['revalidate_path_file_metadata_1']) + aiohttpretty.register_json_uri( + 'GET', + url, + body=root_provider_fixtures['revalidate_path_file_metadata_1'] + ) url = provider.build_url('files', file_id, fields='id, name, mimeType') - aiohttpretty.register_json_uri('GET', url, - body=root_provider_fixtures['revalidate_path_gdoc_file_metadata']) + aiohttpretty.register_json_uri( + 'GET', + url, + body=root_provider_fixtures['revalidate_path_gdoc_file_metadata'] + ) result = await provider.revalidate_path(path, file_name) @@ -359,18 +375,24 @@ async def test_revalidate_path_folder(self, provider, root_provider_fixtures): current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] - name, ext = os.path.splitext(part_name) query = _build_name_search_query(provider, file_name.strip('/') + '/', file_id, True) folder_one_url = provider.build_url('files', q=query, fields='files(id)') - aiohttpretty.register_json_uri('GET', folder_one_url, - body=root_provider_fixtures['revalidate_path_folder_metadata_1']) + aiohttpretty.register_json_uri( + 'GET', + folder_one_url, + body=root_provider_fixtures['revalidate_path_folder_metadata_1'] + ) folder_two_url = provider.build_url('files', file_id, fields='id, name, mimeType') - aiohttpretty.register_json_uri('GET', folder_two_url, - body=root_provider_fixtures['revalidate_path_folder_metadata_2']) + aiohttpretty.register_json_uri( + 'GET', + folder_two_url, + body=root_provider_fixtures['revalidate_path_folder_metadata_2'] + ) result = await provider.revalidate_path(path, file_name, True) + assert result.name in path.name @@ -383,14 +405,23 @@ async def test_upload_create(self, provider, file_stream, root_provider_fixtures file = root_provider_fixtures['list_file']['files'][0] path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], None)) - start_upload_furl = furl.furl(provider._build_upload_url('files', '', - uploadType='resumable')) + start_upload_furl = furl.furl(provider._build_upload_url( + 'files', + '', + uploadType='resumable' + )) start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url - finish_upload_url = provider._build_upload_url('files', uploadType='resumable', - upload_id=upload_id) + finish_upload_url = provider._build_upload_url( + 'files', + uploadType='resumable', + upload_id=upload_id + ) - aiohttpretty.register_uri('POST', start_upload_url, - headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_uri( + 'POST', + start_upload_url, + headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)} + ) aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) result, created = await provider.upload(file_stream, path) @@ -409,14 +440,23 @@ async def test_upload_doesnt_unquote(self, provider, file_stream, root_provider_ file = root_provider_fixtures['list_file']['files'][0] path = GoogleDrivePath('/birdie%2F %20".jpg', _ids=(provider.folder['id'], None)) - start_upload_furl = furl.furl(provider._build_upload_url('files', '', - uploadType='resumable')) + start_upload_furl = furl.furl(provider._build_upload_url( + 'files', + '', + uploadType='resumable' + )) start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url - finish_upload_url = provider._build_upload_url('files', uploadType='resumable', - upload_id=upload_id) + finish_upload_url = provider._build_upload_url( + 'files', + uploadType='resumable', + upload_id=upload_id + ) - aiohttpretty.register_uri('POST', start_upload_url, - headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_uri( + 'POST', + start_upload_url, + headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)} + ) aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) result, created = await provider.upload(file_stream, path) @@ -435,15 +475,26 @@ async def test_upload_update(self, provider, file_stream, root_provider_fixtures file = root_provider_fixtures['list_file']['files'][0] path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], file['id'])) - start_upload_furl = furl.furl(provider._build_upload_url('files', file['id'], - uploadType='resumable')) + start_upload_furl = furl.furl(provider._build_upload_url( + 'files', + file['id'], + uploadType='resumable' + )) start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url - finish_upload_url = provider._build_upload_url('files', path.identifier, - uploadType='resumable', upload_id=upload_id) + finish_upload_url = provider._build_upload_url( + 'files', + path.identifier, + uploadType='resumable', + upload_id=upload_id + ) - aiohttpretty.register_uri('PATCH', start_upload_url, - headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_uri( + 'PATCH', + start_upload_url, + headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)} + ) aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) + result, created = await provider.upload(file_stream, path) assert aiohttpretty.has_call(method='PATCH', uri=start_upload_url) @@ -462,14 +513,25 @@ async def test_upload_create_nested(self, provider, file_stream, root_provider_f _ids=[str(x) for x in range(3)] ) - start_upload_furl = furl.furl(provider._build_upload_url('files', '', - uploadType='resumable')) + start_upload_furl = furl.furl(provider._build_upload_url( + 'files', + '', + uploadType='resumable' + )) start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url - finish_upload_url = provider._build_upload_url('files', uploadType='resumable', - upload_id=upload_id) - aiohttpretty.register_uri('POST', start_upload_url, - headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + finish_upload_url = provider._build_upload_url( + 'files', + uploadType='resumable', + upload_id=upload_id + ) + + aiohttpretty.register_uri( + 'POST', + start_upload_url, + headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)} + ) aiohttpretty.register_json_uri('PUT', finish_upload_url, body=file) + result, created = await provider.upload(file_stream, path) assert aiohttpretty.has_call(method='POST', uri=start_upload_url) @@ -484,18 +546,30 @@ async def test_upload_checksum_mismatch(self, provider, file_stream, root_provid upload_id = '7' path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], None)) - start_upload_furl = furl.furl(provider._build_upload_url('files', '', - uploadType='resumable')) + start_upload_furl = furl.furl(provider._build_upload_url( + 'files', + '', + uploadType='resumable' + )) start_upload_url = start_upload_furl.add(provider.FILE_FIELDS).url - finish_upload_url = provider._build_upload_url('files', uploadType='resumable', - upload_id=upload_id) + finish_upload_url = provider._build_upload_url( + 'files', + uploadType='resumable', + upload_id=upload_id + ) - aiohttpretty.register_json_uri('PUT', finish_upload_url, - body=root_provider_fixtures['checksum_mismatch_metadata']) - aiohttpretty.register_uri('POST', start_upload_url, - headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)}) + aiohttpretty.register_json_uri( + 'PUT', + finish_upload_url, + body=root_provider_fixtures['checksum_mismatch_metadata'] + ) + aiohttpretty.register_uri( + 'POST', + start_upload_url, + headers={'LOCATION': 'http://waterbutler.io?upload_id={}'.format(upload_id)} + ) - with pytest.raises(exceptions.UploadChecksumMismatchError) as exc: + with pytest.raises(exceptions.UploadChecksumMismatchError): await provider.upload(file_stream, path) assert aiohttpretty.has_call(method='PUT', uri=finish_upload_url) @@ -511,10 +585,12 @@ async def test_delete(self, provider, root_provider_fixtures): path = WaterButlerPath('/birdie.jpg', _ids=(None, file['id'])) delete_url = provider.build_url('files', file['id']) del_url_body = json.dumps({'labels': {'trashed': 'true'}}) - aiohttpretty.register_uri('PATCH', - delete_url, - body=del_url_body, - status=200) + aiohttpretty.register_uri( + 'PATCH', + delete_url, + body=del_url_body, + status=HTTPStatus.OK + ) result = await provider.delete(path) @@ -527,15 +603,16 @@ async def test_delete_folder(self, provider, root_provider_fixtures): item = root_provider_fixtures['folder_metadata'] del_url = provider.build_url('files', item['id']) del_url_body = json.dumps({'labels': {'trashed': 'true'}}) - path = WaterButlerPath('/foobar/', _ids=('doesntmatter', item['id'])) - aiohttpretty.register_uri('PATCH', - del_url, - body=del_url_body, - status=200) + aiohttpretty.register_uri( + 'PATCH', + del_url, + body=del_url_body, + status=HTTPStatus.OK + ) - result = await provider.delete(path) + await provider.delete(path) assert aiohttpretty.has_call(method='PATCH', uri=del_url) @@ -550,11 +627,11 @@ async def test_delete_not_existing(self, provider): async def test_delete_root_no_confirm(self, provider): path = WaterButlerPath('/', _ids=('0')) - with pytest.raises(exceptions.DeleteError) as e: + with pytest.raises(exceptions.DeleteError) as exc: await provider.delete(path) - assert e.value.message == 'confirm_delete=1 is required for deleting root provider folder' - assert e.value.code == 400 + assert exc.value.message == 'confirm_delete=1 is required for deleting root provider folder' + assert exc.value.code == HTTPStatus.BAD_REQUEST @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -562,14 +639,20 @@ async def test_delete_root(self, provider, root_provider_fixtures): file = root_provider_fixtures['delete_contents_metadata']['files'][0] root_path = WaterButlerPath('/', _ids=('0')) - url = provider.build_url('files', q="'{}' in parents and trashed = false".format('0'), - fields='files(id)') - aiohttpretty.register_json_uri('GET', url, - body=root_provider_fixtures['delete_contents_metadata']) + url = provider.build_url( + 'files', + q="'{}' in parents and trashed = false".format('0'), + fields='files(id)' + ) + aiohttpretty.register_json_uri( + 'GET', + url, + body=root_provider_fixtures['delete_contents_metadata'] + ) delete_url = provider.build_url('files', file['id']) data = json.dumps({'labels': {'trashed': 'true'}}), - aiohttpretty.register_json_uri('PATCH', delete_url, data=data, status=200) + aiohttpretty.register_json_uri('PATCH', delete_url, data=data, status=HTTPStatus.OK) await provider.delete(root_path, 1) @@ -600,7 +683,8 @@ class TestDownload: JPEG_GOOD_REVISION = GDOC_BAD_REVISION JPEG_BAD_REVISION = GDOC_GOOD_REVISION MAGIC_REVISION = '"LUxk1DXE_0fd4yeJDIgpecr5uPA/MTQ5NTExOTgxMzgzOQ"{}'.format( - ds.DRIVE_IGNORE_VERSION) + provider_settings.DRIVE_IGNORE_VERSION + ) GDOC_EXPORT_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' @@ -622,13 +706,17 @@ async def test_download_editable_gdoc_no_revision(self, provider, sharing_fixtur aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) file_content = b'we love you conrad' - download_file_url = provider.build_url('files', metadata_body['id'], 'export', - mimeType=self.GDOC_EXPORT_MIME_TYPE) + download_file_url = provider.build_url( + 'files', + metadata_body['id'], + 'export', + mimeType=self.GDOC_EXPORT_MIME_TYPE + ) aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path) - assert result.name == 'editable_gdoc.docx' + assert result.name == 'editable_gdoc.docx' content = await result.read() assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -653,8 +741,12 @@ async def test_download_editable_gdoc_good_revision(self, provider, sharing_fixt aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) revision_body = sharing_fixtures['editable_gdoc']['v2_revision'] - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.GDOC_GOOD_REVISION) + revision_url = provider.build_url( + 'files', + metadata_body['id'], + 'revisions', + self.GDOC_GOOD_REVISION + ) revision_url = revision_url.replace('/v3/', '/v2/', 1) aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) @@ -663,8 +755,8 @@ async def test_download_editable_gdoc_good_revision(self, provider, sharing_fixt aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.GDOC_GOOD_REVISION) - assert result.name == 'editable_gdoc.docx' + assert result.name == 'editable_gdoc.docx' content = await result.read() assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -690,15 +782,24 @@ async def test_download_editable_gdoc_bad_revision(self, provider, sharing_fixtu aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) no_such_revision_error = make_no_such_revision_error(self.GDOC_BAD_REVISION) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.GDOC_BAD_REVISION) + revision_url = provider.build_url( + 'files', + metadata_body['id'], + 'revisions', + self.GDOC_BAD_REVISION + ) revision_url = revision_url.replace('/v3/', '/v2/', 1) - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) + aiohttpretty.register_json_uri( + 'GET', + revision_url, + status=HTTPStatus.NOT_FOUND, + body=no_such_revision_error + ) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.download(path, revision=self.GDOC_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND assert aiohttpretty.has_call(method='GET', uri=metadata_url) assert aiohttpretty.has_call(method='GET', uri=revisions_url) assert aiohttpretty.has_call(method='GET', uri=revision_url) @@ -722,13 +823,17 @@ async def test_download_editable_gdoc_magic_revision(self, provider, sharing_fix file_content = b'we love you conrad' mime_type = self.GDOC_EXPORT_MIME_TYPE - download_url = provider.build_url('files', path.identifier, 'export', - mimeType=mime_type) + download_url = provider.build_url( + 'files', + path.identifier, + 'export', + mimeType=mime_type + ) aiohttpretty.register_uri('GET', download_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.MAGIC_REVISION) - assert result.name == 'editable_gdoc.docx' + assert result.name == 'editable_gdoc.docx' content = await result.read() assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -749,13 +854,17 @@ async def test_download_viewable_gdoc_no_revision(self, provider, sharing_fixtur aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_file_url = provider.build_url('files', metadata_body.get('id'), 'export', - mimeType=self.GDOC_EXPORT_MIME_TYPE) + download_file_url = provider.build_url( + 'files', + metadata_body.get('id'), + 'export', + mimeType=self.GDOC_EXPORT_MIME_TYPE + ) aiohttpretty.register_uri('GET', download_file_url, body=file_content, auto_length=True) result = await provider.download(path) - assert result.name == 'viewable_gdoc.docx' + assert result.name == 'viewable_gdoc.docx' content = await result.read() assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -779,15 +888,24 @@ async def test_download_viewable_gdoc_bad_revision(self, provider, sharing_fixtu aiohttpretty.register_json_uri('GET', revisions_url, body=revisions_body) unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) - revision_url = provider.build_url('files', metadata_body['id'], - 'revisions', self.GDOC_BAD_REVISION) + revision_url = provider.build_url( + 'files', + metadata_body['id'], + 'revisions', + self.GDOC_BAD_REVISION + ) revision_url = revision_url.replace('/v3/', '/v2/', 1) - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) + aiohttpretty.register_json_uri( + 'GET', + revision_url, + status=HTTPStatus.NOT_FOUND, + body=unauthorized_error + ) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.download(path, revision=self.GDOC_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -804,13 +922,17 @@ async def test_download_viewable_gdoc_magic_revision(self, provider, sharing_fix file_content = b'we love you conrad' mime_type = self.GDOC_EXPORT_MIME_TYPE - download_url = provider.build_url('files', metadata_body.get('id'), 'export', - mimeType=mime_type) + download_url = provider.build_url( + 'files', + metadata_body.get('id'), + 'export', + mimeType=mime_type + ) aiohttpretty.register_uri('GET', download_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.MAGIC_REVISION) - assert result.name == 'viewable_gdoc.docx' + assert result.name == 'viewable_gdoc.docx' content = await result.read() assert content == file_content assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -854,8 +976,13 @@ async def test_download_editable_jpeg_good_revision(self, provider, sharing_fixt aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) file_content = b'we love you conrad' - download_url = provider.build_url('files', metadata_body.get('id'), - 'revisions', self.JPEG_GOOD_REVISION, alt='media') + download_url = provider.build_url( + 'files', + metadata_body.get('id'), + 'revisions', + self.JPEG_GOOD_REVISION, + alt='media' + ) aiohttpretty.register_uri('GET', download_url, body=file_content, auto_length=True) result = await provider.download(path, revision=self.JPEG_GOOD_REVISION) @@ -879,15 +1006,25 @@ async def test_download_editable_jpeg_bad_revision(self, provider, sharing_fixtu aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) no_such_revision_error = make_no_such_revision_error(self.JPEG_BAD_REVISION) - download_url = provider.build_url('files', metadata_body.get('id'), - 'revisions', self.JPEG_BAD_REVISION, alt='media') - aiohttpretty.register_uri('GET', download_url, status=404, body=no_such_revision_error, - auto_length=True) + download_url = provider.build_url( + 'files', + metadata_body.get('id'), + 'revisions', + self.JPEG_BAD_REVISION, + alt='media' + ) + aiohttpretty.register_uri( + 'GET', + download_url, + status=HTTPStatus.NOT_FOUND, + body=no_such_revision_error, + auto_length=True + ) - with pytest.raises(exceptions.DownloadError) as e: + with pytest.raises(exceptions.DownloadError) as exc: await provider.download(path, revision=self.JPEG_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND assert aiohttpretty.has_call(method='GET', uri=metadata_url) assert aiohttpretty.has_call(method='GET', uri=download_url) @@ -953,15 +1090,25 @@ async def test_download_viewable_jpeg_bad_revision(self, provider, sharing_fixtu aiohttpretty.register_json_uri('GET', metadata_url, body=metadata_body) no_such_revision_error = make_no_such_revision_error(metadata_body['id']) - download_url = provider.build_url('files', metadata_body.get('id'), - 'revisions', self.JPEG_BAD_REVISION, alt='media') - aiohttpretty.register_uri('GET', download_url, status=404, body=no_such_revision_error, - auto_length=True) + download_url = provider.build_url( + 'files', + metadata_body.get('id'), + 'revisions', + self.JPEG_BAD_REVISION, + lt='media' + ) + aiohttpretty.register_uri( + 'GET', + download_url, + status=HTTPStatus.NOT_FOUND, + body=no_such_revision_error, + auto_length=True + ) - with pytest.raises(exceptions.DownloadError) as e: + with pytest.raises(exceptions.DownloadError) as exc: await provider.download(path, revision=self.JPEG_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND assert aiohttpretty.has_call(method='GET', uri=metadata_url) assert aiohttpretty.has_call(method='GET', uri=download_url) @@ -1014,9 +1161,11 @@ class TestMetadata: JPEG_GOOD_REVISION = GDOC_BAD_REVISION JPEG_BAD_REVISION = GDOC_GOOD_REVISION MAGIC_REVISION = '"LUxk1DXE_0fd4yeJDIgpecr5uPA/MTQ5NTExOTgxMzgzOQ"{}'.format( - ds.DRIVE_IGNORE_VERSION) + provider_settings.DRIVE_IGNORE_VERSION + ) - GDOC_EXPORT_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + GDOC_EXPORT_MIME_TYPE = 'application/' \ + 'vnd.openxmlformats-officedocument.wordprocessingml.document' @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1037,20 +1186,26 @@ async def test_metadata_file_root(self, provider, root_provider_fixtures): @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_metadata_string_error_response(self, provider, root_provider_fixtures): - path = WaterButlerPath('/birdie.jpg', - _ids=(provider.folder['id'], - root_provider_fixtures['list_file']['files'][0]['id'])) + path = WaterButlerPath( + '/birdie.jpg', + _ids=(provider.folder['id'], root_provider_fixtures['list_file']['files'][0]['id']) + ) metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) metadata_url = metadata_furl.add(provider.FILE_FIELDS).url - aiohttpretty.register_uri('GET', metadata_url, headers={'Content-Type': 'text/html'}, - body='this is an error message string with a 404... or is it?', status=404) + aiohttpretty.register_uri( + 'GET', + metadata_url, + headers={'Content-Type': 'text/html'}, + body='this is an error message string with a 404... or is it?', + status=HTTPStatus.NOT_FOUND + ) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.metadata(path) - assert e.value.code == 404 - assert e.value.message == 'Could not retrieve file or directory {}'.format('/' + path.path) + assert exc.value.code == HTTPStatus.NOT_FOUND + assert exc.value.message == 'Could not retrieve file or directory {}'.format('/' + path.path) assert aiohttpretty.has_call(method='GET', uri=metadata_url) @pytest.mark.asyncio @@ -1058,10 +1213,10 @@ async def test_metadata_string_error_response(self, provider, root_provider_fixt async def test_metadata_file_root_not_found(self, provider): path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], None)) - with pytest.raises(exceptions.MetadataError) as exc_info: + with pytest.raises(exceptions.MetadataError) as exc: await provider.metadata(path) - assert exc_info.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1087,6 +1242,7 @@ async def test_metadata_file_nested(self, provider): @pytest.mark.aiohttpretty async def test_metadata_root_folder(self, provider, root_provider_fixtures): path = await provider.validate_path('/') + query = provider._build_query(path.identifier) folder_furl = furl.furl(provider.build_url('files', q=query, alt='json', pageSize=1000)) folder_url = folder_furl.add(provider.FOLDER_FIELDS).url @@ -1142,7 +1298,6 @@ async def test_folder_metadata(self, provider, root_provider_fixtures): result = await provider.metadata(path) expected = GoogleDriveFolderMetadata(file, path.child(file['name'], folder=True)) - assert result == [expected] assert aiohttpretty.has_call(method='GET', uri=folder_url) @@ -1182,8 +1337,12 @@ async def test_metadata_editable_gdoc_good_revision(self, provider, sharing_fixt ) revision_body = sharing_fixtures['editable_gdoc']['revision'] - revision_furl = furl.furl(provider.build_url('files', path.identifier, - 'revisions', self.GDOC_GOOD_REVISION)) + revision_furl = furl.furl(provider.build_url( + 'files', + path.identifier, + 'revisions', + self.GDOC_GOOD_REVISION + )) revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) @@ -1203,15 +1362,19 @@ async def test_metadata_editable_gdoc_bad_revision(self, provider, sharing_fixtu ) no_such_revision_error = make_no_such_revision_error(self.GDOC_BAD_REVISION) - revision_furl = furl.furl(provider.build_url('files', path.identifier, - 'revisions', self.GDOC_BAD_REVISION)) + revision_furl = furl.furl(provider.build_url( + 'files', + path.identifier, + 'revisions', + self.GDOC_BAD_REVISION + )) revision_url = revision_furl.add(provider.REVISION_FIELDS).url - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) + aiohttpretty.register_json_uri('GET', revision_url, status=HTTPStatus.NOT_FOUND, body=no_such_revision_error) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.metadata(path, revision=self.GDOC_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND assert aiohttpretty.has_call(method='GET', uri=revision_url) @pytest.mark.asyncio @@ -1235,7 +1398,6 @@ async def test_metadata_editable_gdoc_magic_revision(self, provider, sharing_fix local_metadata = copy.deepcopy(metadata_body) local_metadata['version'] = revisions_body['revisions'][-1]['id'] - expected = GoogleDriveFileMetadata(local_metadata, path) assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1258,9 +1420,8 @@ async def test_metadata_viewable_gdoc_no_revision(self, provider, sharing_fixtur local_metadata = copy.deepcopy(metadata_body) etag = '{}::{}'.format(local_metadata['id'], local_metadata['modifiedTime']) - local_metadata['version'] = etag + ds.DRIVE_IGNORE_VERSION + local_metadata['version'] = etag + provider_settings.DRIVE_IGNORE_VERSION expected = GoogleDriveFileMetadata(local_metadata, path) - assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1274,15 +1435,24 @@ async def test_metadata_viewable_gdoc_bad_revision(self, provider, sharing_fixtu ) unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) - revision_furl = furl.furl(provider.build_url('files', path.identifier, - 'revisions', self.GDOC_BAD_REVISION)) + revision_furl = furl.furl(provider.build_url( + 'files', + path.identifier, + 'revisions', + self.GDOC_BAD_REVISION) + ) revision_url = revision_furl.add(provider.REVISION_FIELDS).url - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) + aiohttpretty.register_json_uri( + 'GET', + revision_url, + status=HTTPStatus.NOT_FOUND, + body=unauthorized_error + ) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.metadata(path, revision=self.GDOC_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1301,7 +1471,7 @@ async def test_metadata_viewable_gdoc_magic_revision(self, provider, sharing_fix local_metadata = copy.deepcopy(metadata_body) etag = '{}::{}'.format(local_metadata['id'], local_metadata['modifiedTime']) - local_metadata['version'] = etag + ds.DRIVE_IGNORE_VERSION + local_metadata['version'] = etag + provider_settings.DRIVE_IGNORE_VERSION expected = GoogleDriveFileMetadata(local_metadata, path) assert result == expected assert aiohttpretty.has_call(method='GET', uri=metadata_url) @@ -1335,8 +1505,12 @@ async def test_metadata_editable_jpeg_good_revision(self, provider, sharing_fixt ) revision_body = sharing_fixtures['editable_jpeg']['revision'] - revision_furl = furl.furl(provider.build_url('files', path.identifier, - 'revisions', self.JPEG_GOOD_REVISION)) + revision_furl = furl.furl(provider.build_url( + 'files', + path.identifier, + 'revisions', + self.JPEG_GOOD_REVISION + )) revision_url = revision_furl.add(provider.REVISION_FIELDS).url aiohttpretty.register_json_uri('GET', revision_url, body=revision_body) @@ -1356,15 +1530,24 @@ async def test_metadata_editable_jpeg_bad_revision(self, provider, sharing_fixtu ) no_such_revision_error = make_no_such_revision_error(self.JPEG_BAD_REVISION) - revision_furl = furl.furl(provider.build_url('files', path.identifier, - 'revisions', self.JPEG_BAD_REVISION)) + revision_furl = furl.furl(provider.build_url( + 'files', + path.identifier, + 'revisions', + self.JPEG_BAD_REVISION + )) revision_url = revision_furl.add(provider.REVISION_FIELDS).url - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=no_such_revision_error) + aiohttpretty.register_json_uri( + 'GET', + revision_url, + status=HTTPStatus.NOT_FOUND, + body=no_such_revision_error + ) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.metadata(path, revision=self.JPEG_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1414,15 +1597,24 @@ async def test_metadata_viewable_jpeg_bad_revision(self, provider, sharing_fixtu ) unauthorized_error = make_unauthorized_file_access_error(metadata_body['id']) - revision_furl = furl.furl(provider.build_url('files', path.identifier, - 'revisions', self.JPEG_BAD_REVISION)) + revision_furl = furl.furl(provider.build_url( + 'files', + path.identifier, + 'revisions', + self.JPEG_BAD_REVISION + )) revision_url = revision_furl.add(provider.REVISION_FIELDS).url - aiohttpretty.register_json_uri('GET', revision_url, status=404, body=unauthorized_error) + aiohttpretty.register_json_uri( + 'GET', + revision_url, + status=HTTPStatus.NOT_FOUND, + body=unauthorized_error + ) - with pytest.raises(exceptions.NotFoundError) as e: + with pytest.raises(exceptions.NotFoundError) as exc: await provider.metadata(path, revision=self.JPEG_BAD_REVISION) - assert e.value.code == 404 + assert exc.value.code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -1453,20 +1645,27 @@ async def test_get_revisions(self, provider, revision_fixtures, root_provider_fi path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', file['id'])) revisions_url = provider.build_url('files', file['id'], 'revisions') - aiohttpretty.register_json_uri('GET', revisions_url, - body=revision_fixtures['revisions_list']) + aiohttpretty.register_json_uri( + 'GET', + revisions_url, + body=revision_fixtures['revisions_list'] + ) result = await provider.revisions(path) + expected = [ - GoogleDriveRevision(each) - for each in revision_fixtures['revisions_list']['revisions'] + GoogleDriveRevision(each) for each in revision_fixtures['revisions_list']['revisions'] ] assert result == expected @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_get_revisions_no_revisions(self, provider, revision_fixtures, - root_provider_fixtures): + async def test_get_revisions_no_revisions( + self, + provider, + revision_fixtures, + root_provider_fixtures + ): file = root_provider_fixtures['list_file']['files'][0] path = WaterButlerPath('/birdie.jpg', _ids=('doesntmatter', file['id'])) @@ -1475,16 +1674,19 @@ async def test_get_revisions_no_revisions(self, provider, revision_fixtures, aiohttpretty.register_json_uri('GET', metadata_url, body=file) revisions_url = provider.build_url('files', file['id'], 'revisions') - - aiohttpretty.register_json_uri('GET', revisions_url, - body=revision_fixtures['revisions_list_empty']) + aiohttpretty.register_json_uri( + 'GET', + revisions_url, + body=revision_fixtures['revisions_list_empty'] + ) result = await provider.revisions(path) + etag = '{}::{}'.format(file['id'], file['modifiedTime']) expected = [ GoogleDriveRevision({ 'modifiedTime': file['modifiedTime'], - 'id': etag + ds.DRIVE_IGNORE_VERSION, + 'id': etag + provider_settings.DRIVE_IGNORE_VERSION, }) ] assert result == expected @@ -1502,14 +1704,19 @@ async def test_get_revisions_for_uneditable(self, provider, sharing_fixtures): revisions_url = provider.build_url('files', file['id'], 'revisions') aiohttpretty.register_json_uri( - 'GET', revisions_url, body=file_fixtures['revisions_error'], status=403) + 'GET', + revisions_url, + body=file_fixtures['revisions_error'], + status=HTTPStatus.FORBIDDEN + ) result = await provider.revisions(path) + etag = '{}::{}'.format(file['id'], file['modifiedTime']) expected = [ GoogleDriveRevision({ 'modifiedTime': file['modifiedTime'], - 'id': etag + ds.DRIVE_IGNORE_VERSION, + 'id': etag + provider_settings.DRIVE_IGNORE_VERSION, }) ] assert result == expected @@ -1528,22 +1735,29 @@ class TestCreateFolder: async def test_already_exists(self, provider): path = WaterButlerPath('/hugo/', _ids=('doesnt', 'matter')) - with pytest.raises(exceptions.FolderNamingConflict) as e: + with pytest.raises(exceptions.FolderNamingConflict) as exc: await provider.create_folder(path) - assert e.value.code == 409 - assert e.value.message == ('Cannot create folder "hugo", because a file or folder ' - 'already exists with that name') + assert exc.value.code == HTTPStatus.CONFLICT + assert exc.value.message == 'Cannot create folder "hugo", ' \ + 'because a file or folder already exists with that name' @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_returns_metadata(self, provider, root_provider_fixtures): + async def test_returns_metadata( + self, + provider, + root_provider_fixtures + ): path = WaterButlerPath('/osf%20test/', _ids=(provider.folder['id'], None)) metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) metadata_url = metadata_furl.add(provider.FILE_FIELDS).url - aiohttpretty.register_json_uri('POST', metadata_url, - body=root_provider_fixtures['folder_metadata']) + aiohttpretty.register_json_uri( + 'POST', + metadata_url, + body=root_provider_fixtures['folder_metadata'] + ) resp = await provider.create_folder(path) @@ -1555,22 +1769,24 @@ async def test_returns_metadata(self, provider, root_provider_fixtures): @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_raises_non_404(self, provider): - path = WaterButlerPath('/hugo/kim/pins/', _ids=(provider.folder['id'], - 'something', 'something', None)) + path = WaterButlerPath( + '/hugo/kim/pins/', + _ids=(provider.folder['id'], 'something', 'something', None) + ) metadata_furl = furl.furl(provider.build_url('files', path.identifier or '')) metadata_url = metadata_furl.add(provider.FILE_FIELDS).url aiohttpretty.register_json_uri('POST', metadata_url, status=418) - with pytest.raises(exceptions.CreateFolderError) as e: + with pytest.raises(exceptions.CreateFolderError) as exc: await provider.create_folder(path) - assert e.value.code == 418 + assert exc.value.code == 418 @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_must_be_folder(self, provider, monkeypatch): - with pytest.raises(exceptions.CreateFolderError) as e: + with pytest.raises(exceptions.CreateFolderError): await provider.create_folder(WaterButlerPath('/carp.fish', _ids=('doesnt', 'matter'))) @@ -1581,21 +1797,29 @@ class TestIntraFunctions: async def test_intra_move_file(self, provider, root_provider_fixtures): file = root_provider_fixtures['docs_file_metadata'] src_path = WaterButlerPath('/unsure.txt', _ids=(provider.folder['id'], file['id'])) - dest_path = WaterButlerPath('/really/unsure.txt', _ids=(provider.folder['id'], - file['id'], file['id'])) + dest_path = WaterButlerPath( + '/really/unsure.txt', + _ids=(provider.folder['id'], file['id'], file['id']) + ) - move_furl = furl.furl(provider.build_url('files', src_path.identifier, - removeParents=src_path.parent.identifier, - addParents=dest_path.parent.identifier)) + move_furl = furl.furl(provider.build_url( + 'files', + src_path.identifier, + removeParents=src_path.parent.identifier, + addParents=dest_path.parent.identifier + )) move_url = move_furl.add(provider.FILE_FIELDS).url - data = json.dumps({ - 'name': dest_path.name - }), + data = json.dumps({'name': dest_path.name}), aiohttpretty.register_json_uri('PATCH', move_url, data=data, body=file) delete_url = provider.build_url('files', file['id']) del_url_body = json.dumps({'trashed': 'true'}) - aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) + aiohttpretty.register_uri( + 'PATCH', + delete_url, + body=del_url_body, + status=HTTPStatus.OK + ) result, created = await provider.intra_move(provider, src_path, dest_path) expected = GoogleDriveFileMetadata(file, dest_path) @@ -1610,36 +1834,38 @@ async def test_intra_move_folder(self, provider, root_provider_fixtures): folder = root_provider_fixtures['folder_metadata'] folder2 = root_provider_fixtures['folder2_metadata'] src_path = WaterButlerPath('/unsure/', _ids=(provider.folder['id'], folder['id'])) - dest_path = WaterButlerPath('/really/unsure/', _ids=(provider.folder['id'], - folder2['id'], folder2['id'])) + dest_path = WaterButlerPath( + '/really/unsure/', + _ids=(provider.folder['id'], folder2['id'], folder2['id']) + ) - move_furl = furl.furl(provider.build_url('files', src_path.identifier, - removeParents=src_path.parent.identifier, - addParents=dest_path.parent.identifier)) + move_furl = furl.furl(provider.build_url( + 'files', + src_path.identifier, + removeParents=src_path.parent.identifier, + addParents=dest_path.parent.identifier + )) move_url = move_furl.add(provider.FILE_FIELDS).url - data = json.dumps({ - 'name': dest_path.name - }), + data = json.dumps({'name': dest_path.name}), aiohttpretty.register_json_uri('PATCH', move_url, data=data, body=folder) delete_url = provider.build_url('files', folder2['id']) del_url_body = json.dumps({'trashed': 'true'}) - aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) + aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=HTTPStatus.OK) query = provider._build_query(src_path.identifier) children_furl = furl.furl(provider.build_url('files', q=query, alt='json', pageSize=1000)) children_url = children_furl.add(provider.FOLDER_FIELDS).url - children_list = generate_list(3, **root_provider_fixtures['folder_metadata']) aiohttpretty.register_json_uri('GET', children_url, body=children_list) result, created = await provider.intra_move(provider, src_path, dest_path) + expected = GoogleDriveFolderMetadata(folder, dest_path) expected.children = [ provider._serialize_file(dest_path.child(file['name']), file) for file in children_list['files'] ] - assert result == expected assert aiohttpretty.has_call(method='PATCH', uri=move_url) assert aiohttpretty.has_call(method='PATCH', uri=delete_url) @@ -1649,22 +1875,22 @@ async def test_intra_move_folder(self, provider, root_provider_fixtures): async def test_intra_copy_file(self, provider, root_provider_fixtures): file = root_provider_fixtures['docs_file_metadata'] src_path = WaterButlerPath('/unsure.txt', _ids=(provider.folder['id'], file['id'])) - dest_path = WaterButlerPath('/really/unsure.txt', _ids=(provider.folder['id'], - file['id'], file['id'])) + dest_path = WaterButlerPath( + '/really/unsure.txt', + _ids=(provider.folder['id'], file['id'], file['id']) + ) copy_furl = furl.furl(provider.build_url('files', src_path.identifier, 'copy')) copy_url = copy_furl.add(provider.FILE_FIELDS).url data = json.dumps({ - 'parents': [{ - 'id': dest_path.parent.identifier - }], + 'parents': [{'id': dest_path.parent.identifier}], 'name': dest_path.name }), aiohttpretty.register_json_uri('POST', copy_url, data=data, body=file) delete_url = provider.build_url('files', file['id']) del_url_body = json.dumps({'trashed': 'true'}) - aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=200) + aiohttpretty.register_uri('PATCH', delete_url, body=del_url_body, status=HTTPStatus.OK) result, created = await provider.intra_copy(provider, src_path, dest_path) expected = GoogleDriveFileMetadata(file, dest_path) @@ -1711,8 +1937,12 @@ def test_path_from_metadata(self, provider, root_provider_fixtures): @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_revalidate_path_file_error(self, provider, root_provider_fixtures, - error_fixtures): + async def test_revalidate_path_file_error( + self, + provider, + root_provider_fixtures, + error_fixtures + ): file_name = '/root/whatever/Gear1.stl' file_id = root_provider_fixtures['revalidate_path_file_metadata_1']['files'][0]['id'] path = GoogleDrivePath(file_name, _ids=['0', file_id, file_id, file_id]) @@ -1722,13 +1952,15 @@ async def test_revalidate_path_file_error(self, provider, root_provider_fixtures current_part = parts.pop(0) part_name, part_is_folder = current_part[0], current_part[1] query = _build_name_search_query(provider, part_name, provider.folder['id'], True) - list_url = provider.build_url('files', q=query, fields='files(id)') - aiohttpretty.register_json_uri('GET', list_url, - body=error_fixtures['parts_file_missing_metadata']) + aiohttpretty.register_json_uri( + 'GET', + list_url, + body=error_fixtures['parts_file_missing_metadata'] + ) - with pytest.raises(exceptions.MetadataError) as e: - result = await provider._resolve_path_to_ids(file_name) + with pytest.raises(exceptions.MetadataError) as exc: + await provider._resolve_path_to_ids(file_name) - assert e.value.message == '{} not found'.format(str(path)) - assert e.value.code == 404 + assert exc.value.message == '{} not found'.format(str(path)) + assert exc.value.code == HTTPStatus.NOT_FOUND