Skip to content
This repository has been archived by the owner on Aug 11, 2021. It is now read-only.

Commit

Permalink
Merge pull request #38 from l-vo/add_gdrive_uploader
Browse files Browse the repository at this point in the history
Add Google Drive uploader
  • Loading branch information
l-vo authored Nov 4, 2018
2 parents 4e482df + 493c817 commit 4757f39
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 5 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Build Status](https://travis-ci.org/l-vo/photos-picker.svg?branch=master)](https://travis-ci.org/l-vo/photos-picker)
[![codecov](https://codecov.io/gh/l-vo/photos-picker/branch/master/graph/badge.svg)](https://codecov.io/gh/l-vo/photos-picker)

This libary allows to pick photos in a folder according to a given strategy (last photos, random photos...) and copy them to a destination (another folder, Dropbox folder...)
This libary allows to pick photos in a folder according to a given strategy (last photos, random photos...) and copy them to a destination (another system folder, Dropbox or Google drive folder...)

## Compatibility
This library works and is tested with Python 2.7. Other Python versions are not tested yet.
Expand Down Expand Up @@ -55,6 +55,7 @@ Note that uploaders don't append new photos. Either the directory must be empty

* `FilesystemUploader`: copy the photos to a given directory. This directory must exist and be empty.
* `DropBoxUploader`: upload the photos to Dropbox. Note that you should limit your token access to application. Creating a full access token is not needed and may induce security issues.
* `GDriveUploader` upload the photos to Google Drive.

More details [here](doc/uploaders.md)

Expand Down
40 changes: 39 additions & 1 deletion doc/uploaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,42 @@ Upload the photos to Dropbox. ***Be careful, the script empty the `/photos` dire
```python
def __init__(self, api_token)
```
* `api_token` (string): authorization token from Dropbox for accessing API
* `api_token` (string): authorization token from Dropbox for accessing API

## GDriveUploader

### Utility
Upload the photos to Google Drive. ***The script empty a directory named photos-picker. Take care that a directory with a such name doesn't already exist in the used drive account***.

### Constructor arguments
```python
def __init__(self, gauth):
```
* `gauth` (pydrive.auth.GoogleAuth): authentified instance of GoogleAuth.

### GoogleAuth authentication
This library uses pyDrive for dealing with Google Drive API. For allowing your application to use Google Drive API, you must create a `client_secrets.json` credential file at the root of your application. How to get this file is well documented here: [pyDrive quickstart](https://pythonhosted.org/PyDrive/quickstart.html).

Once your application is authenticated, It will be asked to your application end-user to give access to its drive to your application. It's your application resonsability to memorize the authorization of your end-user. Otherwise, the authorization will be asked at each time. Here is an example :
```python
from pydrive.auth import GoogleAuth
from photospicker.uploader.gdrive_uploader import GDriveUploader

# Example from https://stackoverflow.com/questions/24419188/automating-pydrive-verification-process
gauth = GoogleAuth()
# Try to load saved client credentials
gauth.LoadCredentialsFile("mycreds.txt")
if gauth.credentials is None:
# Authenticate if they're not there
gauth.LocalWebserverAuth()
elif gauth.access_token_expired:
# Refresh them if expired
gauth.Refresh()
else:
# Initialize the saved creds
gauth.Authorize()
# Save the current credentials to a file
gauth.SaveCredentialsFile("mycreds.txt")

uploader = GDriveUploader(gauth)
```
1 change: 1 addition & 0 deletions photospicker/exception/uploader_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ class UploaderException(AbstractException):
# Error constants
NOT_FOUND = 1
NOT_EMPTY = 2
MANY_DIRS = 3
65 changes: 65 additions & 0 deletions photospicker/uploader/gdrive_uploader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from io import BytesIO
from photospicker.exception.uploader_exception import UploaderException
from photospicker.uploader.abstract_uploader import AbstractUploader
from pydrive.drive import GoogleDrive


class GDriveUploader(AbstractUploader):
"""Upload picked photo to Google Drive"""

def __init__(self, gauth):
"""
Constructor
:param pydrive.auth.GoogleAuth gauth: GoogleAth authentified instance
"""
super(GDriveUploader, self).__init__()
self._gdrive = GoogleDrive(gauth)
self._folder = None

def initialize(self):
"""Clear remote directory"""
query = "mimeType = 'application/vnd.google-apps.folder'"\
+ " and title = 'photos-picker' and trashed=false"
folders = self._gdrive.ListFile({"q": query}).GetList()

count = len(folders)

# Remove old folder if exists
if count == 0:
# Create folder
folder_metadata = {
'title': 'photos-picker',
'mimeType': 'application/vnd.google-apps.folder'
}
self._folder = self._gdrive.CreateFile(folder_metadata)
self._folder.Upload()
elif count == 1:
self._folder = folders[0]
# Remove previously uploaded files
query = "'{folder_id}' in parents and trashed=false"
files = self._gdrive.ListFile(
{"q": query.format(folder_id=self._folder['id'])}
).GetList()
for file_to_delete in files:
file_to_delete.Delete()
else:
raise UploaderException(
UploaderException.MANY_DIRS,
"Many dirs named photos-picker; can't continue"
)

def upload(self, binary, original_filename):
"""
Upload or copy files to destination
:param str binary : binary data to upload
:param str original_filename: original file name
"""
filename = self._build_filename(original_filename)
gfile = self._gdrive.CreateFile({
'title': filename,
"parents": [{"kind": "drive#fileLink", "id": self._folder['id']}]
})
gfile.content = BytesIO(binary)
gfile.Upload()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ tox
flake8
coverage
codecov
callee
callee
pydrive
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ def read(fname):

setup(
name='photos-picker',
version='0.2.1',
version='0.3.0',
description='Pick photos following a given strategy and upload them to various destinations',
author='Laurent VOULLEMIER',
author_email='[email protected]',
url='https://github.com/l-vo/photos-picker',
packages=find_packages(),
install_requires=['Pillow', 'zope.event', 'dropbox'],
install_requires=['Pillow', 'zope.event', 'dropbox', 'pydrive'],
include_package_data=True,
zip_safe=False,
long_description=read('README.md'),
Expand Down
185 changes: 185 additions & 0 deletions tests/uploader/test_gdrive_uploader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
from unittest import TestCase
from mock import Mock, MagicMock
from photospicker.exception.uploader_exception import UploaderException
from photospicker.uploader.gdrive_uploader import GDriveUploader
import mock


class TestGDriveUploader(TestCase):
"""Test class for GDriveUploader"""

def setUp(self):
self._gauth = Mock()

@mock.patch('photospicker.uploader.gdrive_uploader.GoogleDrive')
def test_initialize_should_throw_exception_if_many_folders(
self,
gdrive_constructor_mock
):
"""
Test that an exception is thrown if there is many directories
with the name of the photos picker directory
:param MagicMock gdrive_constructor_mock:
mock for gdrive constructor
"""
self._initialize_gdrive_mock(
gdrive_constructor_mock,
[[Mock(), Mock()]]
)

with self.assertRaises(UploaderException) as cm:
sut = GDriveUploader(self._gauth)
sut.initialize()

self._initialize_common_assertions(gdrive_constructor_mock)

self.assertEqual(UploaderException.MANY_DIRS, cm.exception.code)

@mock.patch('photospicker.uploader.gdrive_uploader.GoogleDrive')
def test_initialize_with_no_existing_folder_should_create_folder(
self,
gdrive_constructor_mock
):
"""
Test that if no folder exists with the name of the photos picker
directory, it is created
:param MagicMock gdrive_constructor_mock:
mock for gdrive constructor
"""
gdrive_mock = self._initialize_gdrive_mock(
gdrive_constructor_mock,
[[]]
)

created_folder_mock = Mock()
gdrive_mock.CreateFile.return_value = created_folder_mock

sut = GDriveUploader(self._gauth)
sut.initialize()

gdrive_mock.CreateFile.assert_called_once_with({
'mimeType': 'application/vnd.google-apps.folder',
'title': 'photos-picker'
})
created_folder_mock.Upload.assert_called_once()

self._initialize_common_assertions(gdrive_constructor_mock)

@mock.patch('photospicker.uploader.gdrive_uploader.GoogleDrive')
def test_initialize_with_existing_folder_should_empty_it(
self,
gdrive_constructor_mock
):
"""
Test that if a folder already exists with the name of the photos picker
directory, it is emptied
:param MagicMock gdrive_constructor_mock:
mock for gdrive constructor
"""
folder_mock = MagicMock()
folder_mock.__getitem__.return_value = 7
file1 = Mock()
file2 = Mock()
self._initialize_gdrive_mock(
gdrive_constructor_mock,
[[folder_mock], [file1, file2]]
)

sut = GDriveUploader(self._gauth)
sut.initialize()

folder_mock.__getitem__.assert_called_once_with('id')

self._initialize_common_assertions(
gdrive_constructor_mock,
[mock.call({'q': "'7' in parents and trashed=false"})]
)

file1.Delete.assert_called_once()
file2.Delete.assert_called_once()

@staticmethod
def _initialize_gdrive_mock(gdrive_constructor_mock, return_values):
"""
Create a mock for gdrive GetList method
:param MagicMock gdrive_constructor_mock:
mock for gdrive constructor
:param list return_values: values successively
returned by GetList method
:return Mock
"""
gdrive_mock = Mock()
gdrive_constructor_mock.return_value = gdrive_mock

side_effect = []
for return_value in return_values:
gdrive_list_mock = Mock()
gdrive_list_mock.GetList.return_value = return_value
side_effect.append(gdrive_list_mock)

gdrive_mock.ListFile.side_effect = side_effect

return gdrive_mock

def _initialize_common_assertions(
self,
gdrive_constructor_mock,
list_files_additonal_calls=[]
):
"""
Make common assertions for initialize tests
:param MagicMock gdrive_constructor_mock:
mock for gdrive constructor
:param list list_files_additonal_calls:
additional expected calls for ListFile method
"""
gdrive_constructor_mock.assert_called_once_with(self._gauth)

calls = [
mock.call({
'q': "mimeType = 'application/vnd.google-apps.folder' "
"and title = 'photos-picker' and trashed=false"
})
]

calls += list_files_additonal_calls

gdrive_constructor_mock.return_value.ListFile.assert_has_calls(calls)

@mock.patch('photospicker.uploader.gdrive_uploader.GoogleDrive')
def test_upload(self, gdrive_constructor_mock):
"""
Test upload method
:param MagicMock gdrive_constructor_mock:
mock for gdrive constructor
"""
folder_mock = MagicMock()
folder_mock.__getitem__.return_value = 12
gdrive_mock = self._initialize_gdrive_mock(
gdrive_constructor_mock,
[[folder_mock], []]
)

created_file_mock = Mock()
gdrive_mock.CreateFile.return_value = created_file_mock

sut = GDriveUploader(self._gauth)
sut.initialize()
sut.upload('mybinarydata', 'IMG5423.JPG')

folder_mock.__getitem__.assert_called_with('id')

gdrive_mock.CreateFile.assert_called_once_with({
'parents': [{'kind': 'drive#fileLink', 'id': 12}],
'title': 'photo0.jpg'
})

self.assertEqual('mybinarydata', created_file_mock.content.getvalue())
created_file_mock.Upload.assert_called_once()

0 comments on commit 4757f39

Please sign in to comment.