├── certificate
└── empty
├── docs
├── _static
│ └── Empty
├── _templates
│ └── Empty
├── requirements.txt
├── index.rst
├── Makefile
└── conf.py
├── MANIFEST.in
├── gdstorage
├── __init__.py
├── apps.py
├── tests.py
└── storage.py
├── test
├── gdrive_logo.png
└── settings.py
├── .readthedocs.yaml
├── .github
└── workflows
│ ├── lint.yml
│ └── tests.yml
├── .gitignore
├── LICENSE.txt
├── setup.py
└── README.rst
/certificate/empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/_static/Empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/_templates/Empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx==4.3.1
2 | sphinx-rtd-theme
3 | sphinx-rtd-dark-mode
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE.txt
2 | include README.rst
3 | exclude gdstorage/tests.py
--------------------------------------------------------------------------------
/gdstorage/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'gdstorage.apps.GoogleDriveStorageConfig'
2 |
--------------------------------------------------------------------------------
/test/gdrive_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/torre76/django-googledrive-storage/HEAD/test/gdrive_logo.png
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | version: 2
6 |
7 | build:
8 | os: ubuntu-20.04
9 | tools:
10 | python: "3.8"
11 |
12 | sphinx:
13 | configuration: docs/conf.py
14 |
15 | python:
16 | install:
17 | - requirements: docs/requirements.txt
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: Set up Python
11 | uses: actions/setup-python@v2
12 | - name: Install dependencies
13 | run: pip install flake8 flake8-quotes flake8-isort flake8-print
14 | - name: Run lint
15 | run: cd gdstorage && flake8
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .project
2 | .pydevproject
3 | 89e3e07e0dc77f40c981b900c87d6246866172b7-privatekey.p12
4 | django_googledrive_storage
5 | *.pyc
6 | manage.py
7 | dist
8 | gdstorage.egg-info
9 | db.sqlite3
10 | django_googledrive_storage.egg-info
11 | .idea/
12 | venv/
13 | certificate/89e3e07e0dc77f40c981b900c87d6246866172b7-privatekey.crt
14 | certificate/89e3e07e0dc77f40c981b900c87d6246866172b7-privatekey.key
15 | certificate/Django Google Drive Storage-44bb261c98ae.key
16 | certificate/Django Google Drive Storage-44bb261c98ae.p12
17 | build
18 | docs/test
19 | docs/out
20 | .ropeproject
21 | .vscode/
22 | gdtest/
23 | docs/_build
--------------------------------------------------------------------------------
/gdstorage/apps.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.apps import AppConfig
4 | from django.conf import settings
5 | from django.core.exceptions import ImproperlyConfigured
6 |
7 |
8 | class GoogleDriveStorageConfig(AppConfig):
9 | name = 'gdstorage'
10 | verbose_name = 'Google Drive Storage'
11 | _prefix = 'GOOGLE_DRIVE_STORAGE'
12 |
13 | def ready(self):
14 | if not hasattr(settings, self._get_attr('JSON_KEY_FILE')):
15 | if not os.getenv(self._get_attr('JSON_KEY_FILE_CONTENTS')):
16 | raise ImproperlyConfigured(
17 | 'Either GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE in settings '
18 | 'or GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE_CONTENTS '
19 | 'environment variable should be defined.'
20 | )
21 | if not hasattr(settings, self._get_attr('MEDIA_ROOT')):
22 | setattr(settings, self._get_attr('MEDIA_ROOT'), '')
23 |
24 | def _get_attr(self, suffix):
25 | return '_'.join([self._prefix, suffix])
26 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ['3.6', '3.7', '3.8', '3.9']
11 | django-version: ['2.2.*', '3.0.*', '3.1.*']
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Set up Python ${{ matrix.python-version }} / Django ${{ matrix.django-version }}
15 | uses: actions/setup-python@v2
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Install dependencies
19 | run: |
20 | pip install -U pip
21 | pip install -U Django==${{ matrix.django-version }} pytest-django
22 | - name: Run tests
23 | run: |
24 | python setup.py install
25 | django-admin.py startproject gdtest
26 | cp -av test/settings.py gdtest/gdtest/
27 | cp -av gdstorage/tests.py gdtest/gdtest/
28 | cp -av test/ gdtest/
29 | cd gdtest && pytest -v --ds gdtest.settings gdtest/tests.py
30 | env:
31 | GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE_CONTENTS: ${{ secrets.GDSTORAGE_KEY_FILE_CONTENT }}
32 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Gian Luca Dalla Torre
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | 1. Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | 2. Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | 3. The names of its contributors may not be used to endorse or promote products
12 | derived from this software without specific prior written permission.
13 |
14 | THIS SOFTWARE IS PROVIDED BY GIAN LUCA DALLA TORRE ''AS IS'' AND ANY
15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL GIAN LUCA DALLA TORRE BE LIABLE FOR ANY
18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | import setuptools
3 | import sys
4 |
5 | long_description = codecs.open('README.rst', "r").read()
6 |
7 | # Conditional load of enumeration
8 | # See https://hynek.me/articles/conditional-python-dependencies/
9 |
10 | INSTALL_REQUIRES = [
11 | "google-api-python-client >= 1.8.2",
12 | "google-auth >= 1.28.0,<2",
13 | "python-dateutil >= 2.5.3",
14 | "Django >= 2.2"
15 | ]
16 |
17 | setuptools.setup(
18 | name="django-googledrive-storage",
19 | version="1.6.0",
20 | author="Gian Luca Dalla Torre",
21 | author_email="gianluca.dallatorre@gmail.com",
22 | description=(
23 | "Storage implementation for Django that interacts with Google Drive"),
24 | license="LICENSE.txt",
25 | keywords="django google drive storage googledrive",
26 | url="https://github.com/torre76/django-googledrive-storage",
27 | download_url="https://github.com/torre76/django-googledrive-storage/tarball/1.6.0",
28 | packages=setuptools.find_packages(
29 | exclude=["django_googledrive_storage", "gdstorage.tests", "docs"]),
30 | long_description=long_description,
31 | package_data={
32 | '': ['README.rst'],
33 | },
34 | install_requires=INSTALL_REQUIRES,
35 | classifiers=[
36 | "Development Status :: 5 - Production/Stable",
37 | "Framework :: Django",
38 | "License :: OSI Approved :: BSD License",
39 | "Programming Language :: Python :: 3.6",
40 | "Programming Language :: Python :: 3.7",
41 | "Programming Language :: Python :: 3.8",
42 | "Programming Language :: Python :: 3.9",
43 | "Programming Language :: Python :: 3.10"
44 | ],
45 | )
46 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===========================
2 | Django Google Drive Storage
3 | ===========================
4 |
5 | |build-status| |lint| |pypi| |docs|
6 |
7 | This is a `Django Storage `_ implementation that uses `Google Drive `_ as backend for storing data.
8 |
9 | Quick start
10 | -----------
11 |
12 | Installation
13 | ************
14 |
15 | .. code-block:: bash
16 |
17 | pip install django-googledrive-storage
18 |
19 |
20 | Setup
21 | *****
22 |
23 | .. code-block:: python
24 |
25 | INSTALLED_APPS = (
26 | ...,
27 | 'django.contrib.staticfiles',
28 | 'gdstorage'
29 | )
30 |
31 | Set the environment variable `GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE_CONTENTS` or path file in the `settings.py`: `GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE`.
32 |
33 | Optional set `GOOGLE_DRIVE_STORAGE_MEDIA_ROOT` in the `settings.py`
34 |
35 | Usage example
36 | *************
37 |
38 | .. code-block:: python
39 |
40 | from gdstorage.storage import GoogleDriveStorage
41 |
42 | # Define Google Drive Storage
43 | gd_storage = GoogleDriveStorage()
44 |
45 | ...
46 |
47 | class Map(models.Model):
48 | id = models.AutoField( primary_key=True)
49 | map_name = models.CharField(max_length=200)
50 | map_data = models.FileField(upload_to='maps', storage=gd_storage)
51 |
52 |
53 | Documentation and Installation instructions
54 | -------------------------------------------
55 |
56 | Documentation and installation instructions can be found at `Read The Docs `_.
57 |
58 | .. |build-status| image:: https://github.com/conformist-mw/django-googledrive-storage/workflows/tests/badge.svg
59 | :target: https://github.com/conformist-mw/django-googledrive-storage/actions/workflows/tests.yml
60 | :alt: Tests status
61 |
62 | .. |lint| image:: https://github.com/conformist-mw/django-googledrive-storage/workflows/lint/badge.svg
63 | :target: https://github.com/conformist-mw/django-googledrive-storage/actions/workflows/lint.yml
64 | :alt: Linter status
65 |
66 | .. |pypi| image:: https://img.shields.io/pypi/v/django-googledrive-storage.svg
67 | :target: https://pypi.python.org/pypi/django-googledrive-storage/
68 | :alt: django-googledrive-storage on Pypi
69 |
70 | .. |docs| image:: https://readthedocs.org/projects/django-googledrive-storage/badge/?version=latest
71 | :target: http://django-googledrive-storage.readthedocs.org/en/latest/?badge=latest
72 | :alt: Documentation Status
73 |
--------------------------------------------------------------------------------
/test/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for gdtest project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.10b1.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/dev/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/dev/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '&$)zvzgmzpvnot6_p7_6u@ea$@c7^j*(0%9g+iy%=yju07k%'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'gdstorage'
41 | ]
42 |
43 | MIDDLEWARE = [
44 | 'django.middleware.security.SecurityMiddleware',
45 | 'django.contrib.sessions.middleware.SessionMiddleware',
46 | 'django.middleware.common.CommonMiddleware',
47 | 'django.middleware.csrf.CsrfViewMiddleware',
48 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
49 | 'django.contrib.messages.middleware.MessageMiddleware',
50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
51 | ]
52 |
53 | ROOT_URLCONF = 'gdtest.urls'
54 |
55 | TEMPLATES = [
56 | {
57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
58 | 'DIRS': [],
59 | 'APP_DIRS': True,
60 | 'OPTIONS': {
61 | 'context_processors': [
62 | 'django.template.context_processors.debug',
63 | 'django.template.context_processors.request',
64 | 'django.contrib.auth.context_processors.auth',
65 | 'django.contrib.messages.context_processors.messages',
66 | ],
67 | },
68 | },
69 | ]
70 |
71 | WSGI_APPLICATION = 'gdtest.wsgi.application'
72 |
73 |
74 | # Database
75 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases
76 |
77 | DATABASES = {
78 | 'default': {
79 | 'ENGINE': 'django.db.backends.sqlite3',
80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
81 | }
82 | }
83 |
84 |
85 | # Password validation
86 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
87 |
88 | AUTH_PASSWORD_VALIDATORS = [
89 | {
90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
91 | },
92 | {
93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
94 | },
95 | {
96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
97 | },
98 | {
99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
100 | },
101 | ]
102 |
103 |
104 | # Internationalization
105 | # https://docs.djangoproject.com/en/dev/topics/i18n/
106 |
107 | LANGUAGE_CODE = 'en-us'
108 |
109 | TIME_ZONE = 'UTC'
110 |
111 | USE_I18N = True
112 |
113 | USE_L10N = True
114 |
115 | USE_TZ = True
116 |
117 |
118 | # Static files (CSS, JavaScript, Images)
119 | # https://docs.djangoproject.com/en/dev/howto/static-files/
120 |
121 | STATIC_URL = '/static/'
122 |
123 | #
124 | # Google Drive Storage Settings
125 | #
126 | GOOGLE_DRIVE_STORAGE_SERVICE_EMAIL = '61034839021-r18v4c9k072dud32iook8pv8meaie8vv@developer.gserviceaccount.com'
--------------------------------------------------------------------------------
/gdstorage/tests.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | import time
4 |
5 | import pytest
6 |
7 | from gdstorage.storage import (GoogleDriveFilePermission,
8 | GoogleDrivePermissionRole,
9 | GoogleDrivePermissionType, GoogleDriveStorage)
10 |
11 | SLEEP_INTERVAL = 10
12 |
13 |
14 | @pytest.fixture
15 | def gds():
16 | return GoogleDriveStorage()
17 |
18 |
19 | @pytest.fixture
20 | def write_perm_gds():
21 | return GoogleDriveStorage(
22 | permissions=(GoogleDriveFilePermission(
23 | GoogleDrivePermissionRole.WRITER,
24 | GoogleDrivePermissionType.ANYONE,
25 | ),)
26 | )
27 |
28 |
29 | @pytest.fixture
30 | def read_write_perm_gds():
31 | return GoogleDriveStorage(
32 | permissions=(
33 | GoogleDriveFilePermission(
34 | GoogleDrivePermissionRole.WRITER,
35 | GoogleDrivePermissionType.USER,
36 | 'foo@mailinator.com',
37 | ),
38 | GoogleDriveFilePermission(
39 | GoogleDrivePermissionRole.READER,
40 | GoogleDrivePermissionType.ANYONE,
41 | ),
42 | )
43 | )
44 |
45 |
46 | class TestGoogleDriveStorage:
47 | def test_check_root_file_exists(self, gds):
48 | file_data = gds._check_file_exists('How to get started with Drive')
49 | assert file_data, "Unable to find file 'How to get started with Drive'"
50 | time.sleep(SLEEP_INTERVAL)
51 |
52 | def test_check_or_create_folder(self, gds):
53 | folder_data = gds._get_or_create_folder('test4/folder')
54 | assert folder_data, "Unable to find or create folder 'test4/folder'"
55 | time.sleep(SLEEP_INTERVAL)
56 |
57 | def _test_upload_file(self, gds):
58 | file_name = os.path.join(
59 | os.path.dirname(os.path.abspath(__file__)),
60 | '../test/gdrive_logo.png',
61 | )
62 | with open(file_name, 'rb') as file:
63 | result = gds.save('/test4/gdrive_logo.png', file)
64 | assert result, 'Unable to upload file to Google Drive'
65 |
66 | def _test_list_folder(self, gds):
67 | self._test_upload_file(gds)
68 | directories, files = gds.listdir('/test4')
69 | assert len(files) > 0, 'Unable to read directory data'
70 |
71 | def _test_open_file(self):
72 | gds = GoogleDriveStorage()
73 | self._test_list_folder(gds)
74 | file = gds.open('/test4/gdrive_logo.png', 'rb')
75 | assert file, 'Unable to load data from Google Drive'
76 |
77 | def test_permission_full_write(self, write_perm_gds):
78 | file_name = os.path.join(
79 | os.path.dirname(os.path.abspath(__file__)),
80 | '../test/gdrive_logo.png',
81 | )
82 | with open(file_name, 'rb') as file:
83 | result = write_perm_gds.save('/test4/gdrive_logo.png', file)
84 | assert result, 'Unable to upload file to Google Drive'
85 | file = write_perm_gds.open(result, 'rb')
86 | assert file, 'Unable to load data from Google Drive'
87 | time.sleep(SLEEP_INTERVAL)
88 |
89 | def test_multiple_permission(self, read_write_perm_gds):
90 |
91 | file_name = os.path.join(
92 | os.path.dirname(os.path.abspath(__file__)),
93 | '../test/gdrive_logo.png',
94 | )
95 | result = read_write_perm_gds.save(
96 | '/test4/gdrive_logo.png', open(file_name, 'rb')
97 | )
98 | assert result, 'Unable to upload file to Google Drive'
99 | file = read_write_perm_gds.open(result, 'rb')
100 | assert file, 'Unable to load data from Google Drive'
101 | time.sleep(SLEEP_INTERVAL)
102 |
103 | def test_upload_big_file(self, gds):
104 | file_name = os.path.join(
105 | os.path.dirname(os.path.abspath(__file__)),
106 | '../test/huge_file',
107 | )
108 | with open(file_name, 'wb') as out:
109 | out.truncate(1024 * 1024 * 20)
110 |
111 | with open(file_name, 'rb') as file:
112 | result = gds.save('/test5/huge_file', file)
113 | assert result, 'Unable to upload file to Google Drive'
114 |
115 | os.remove(file_name)
116 | time.sleep(SLEEP_INTERVAL)
117 |
118 | def test_open_big_file(self, gds):
119 | self._test_list_folder(gds)
120 | file = gds.open('/test5/huge_file', 'rb')
121 | assert file, 'Unable to load data from Google Drive'
122 | time.sleep(SLEEP_INTERVAL)
123 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. # define a hard line break for HTML
2 | .. |br| raw:: html
3 |
4 |
5 |
6 | Django Google Drive Storage
7 | ===========================
8 |
9 | `Django Google Drive Storage `_
10 | is a `Django Storage `_
11 | implementation that uses `Google Drive `_ as a backend for storing data.
12 |
13 | Please take note that with **this implementation you could not save or load data from a user's Drive**.
14 | You can use only a Drive **dedicated to a Google Project**. This means that:
15 |
16 | * this storage interacts with Google Drive as a Google Project, not a Google User.
17 | * your project can use Google Drive only through `Google Drive SDK `_. Because no user is associated with this Drive, **you cannot use Google Drive User Interface**.
18 | * this storage authenticates with Google using public private keys. See prerequisites_ for how to obtain it.
19 |
20 | Having stated that, with this storage you gain a 15GB space hosted on Google Server where you are able to store data
21 | using Django models.
22 |
23 | .. _prerequisites:
24 |
25 | Prerequisites
26 | *************
27 |
28 | To use this storage, you have to:
29 |
30 | * `set up a project and application in the Google Developers Console `_
31 | * `obtain the json private key file (OAuth 2.0 for Server to Server Applications) for your Google Project associated with Google Drive service `_
32 |
33 | Installation
34 | ************
35 |
36 | This storage is hosted on `PyPI `_. It can be easily installed
37 | through *pip*:
38 |
39 | .. code-block:: bash
40 |
41 | pip install django-googledrive-storage
42 |
43 | Setup
44 | *****
45 |
46 | Once installed, there are a few steps to configure the storage:
47 |
48 | * add the module *gdstorage* to your installed apps in your `settings.py` file:
49 |
50 | .. code-block:: python
51 |
52 | INSTALLED_APPS = (
53 | ...,
54 | 'django.contrib.staticfiles',
55 | 'gdstorage'
56 | )
57 |
58 | * create a section in your `setting.py` that contains the configuration for this storage:
59 |
60 | .. code-block:: python
61 |
62 | #
63 | # Google Drive Storage Settings
64 | #
65 |
66 | GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE = ''
67 | GOOGLE_DRIVE_STORAGE_MEDIA_ROOT = '' # OPTIONAL
68 |
69 | The `GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE` must be the path to *private json key file* obtained by Google. |br|
70 | Alternatively, you can place the contents of your json private key file into an environment variable named
71 | `GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE_CONTENTS`, this requires setting `GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE` to `None`.
72 |
73 | The `GOOGLE_DRIVE_STORAGE_MEDIA_ROOT` is analogous to MEDIA_ROOT for django’s built-in FileSystemStorage
74 |
75 | * instantiate the storage on you `models.py` file before using into the models:
76 |
77 | .. code-block:: python
78 |
79 | from gdstorage.storage import GoogleDriveStorage
80 |
81 | # Define Google Drive Storage
82 | gd_storage = GoogleDriveStorage()
83 |
84 | Use
85 | ***
86 |
87 | Once configured, it can be used as storage space associated with Django:
88 |
89 | .. code-block:: python
90 |
91 | class Map(models.Model):
92 | id = models.AutoField( primary_key=True)
93 | map_name = models.CharField(max_length=200)
94 | map_data = models.FileField(upload_to='maps', storage=gd_storage)
95 |
96 | .. note::
97 |
98 | You can get the `upload_to` parameter to ignore `GOOGLE_DRIVE_STORAGE_MEDIA_ROOT` by using an absolute path
99 | e.g `/maps`
100 |
101 |
102 | File permissions
103 | ****************
104 |
105 | Using the storage this way, all files will be saved as publicly available for read (which is the most common use case),
106 | but sometimes you could have different reason to use Google Storage.
107 |
108 | It is possible to specify a set of file permissions [#google_drive_permissions]_ to change how the file could be read or
109 | written.
110 |
111 | This code block will assign read only capabilities only to the user identified by `foo@mailinator.com`.
112 |
113 | .. code-block:: python
114 |
115 | from gdstorage.storage import GoogleDriveStorage, GoogleDrivePermissionType, GoogleDrivePermissionRole, GoogleDriveFilePermission
116 |
117 | permission = GoogleDriveFilePermission(
118 | GoogleDrivePermissionRole.READER,
119 | GoogleDrivePermissionType.USER,
120 | "foo@mailinator.com"
121 | )
122 |
123 | gd_storage = GoogleDriveStorage(permissions=(permission, ))
124 |
125 | class Map(models.Model):
126 | id = models.AutoField( primary_key=True)
127 | map_name = models.CharField(max_length=200)
128 | map_data = models.FileField(upload_to='maps/', storage=gd_storage)
129 |
130 | Source and License
131 | ******************
132 |
133 | Source can be found on `GitHub `_ with its included
134 | `license `_.
135 |
136 |
137 | .. rubric:: Footnotes
138 |
139 | .. [#google_drive_permissions] A detailed explanation of Google Drive API permission can be found `here `_.
140 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoGoogleDriveStorage.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoGoogleDriveStorage.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoGoogleDriveStorage"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoGoogleDriveStorage"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
178 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Django Google Drive Storage documentation build configuration file, created by
4 | # sphinx-quickstart on Mon Dec 29 13:51:34 2014.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import datetime
16 | import os
17 | import sphinx_rtd_theme
18 |
19 | # If extensions (or modules to document with autodoc) are in another directory,
20 | # add these directories to sys.path here. If the directory is relative to the
21 | # documentation root, use os.path.abspath to make it absolute, like shown here.
22 | # sys.path.insert(0, os.path.abspath('.'))
23 |
24 | # -- General configuration ------------------------------------------------
25 |
26 | # If your documentation needs a minimal Sphinx version, state it here.
27 | # needs_sphinx = '1.0'
28 |
29 | # Add any Sphinx extension module names here, as strings. They can be
30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
31 | # ones.
32 | extensions = [
33 | 'sphinx.ext.autodoc',
34 | 'sphinx.ext.intersphinx',
35 | 'sphinx.ext.todo',
36 | 'sphinx.ext.ifconfig',
37 | 'sphinx.ext.viewcode',
38 | 'sphinx_rtd_dark_mode',
39 | ]
40 |
41 | # Add any paths that contain templates here, relative to this directory.
42 | templates_path = ['_templates']
43 |
44 | # The suffix of source filenames.
45 | source_suffix = '.rst'
46 |
47 | # The encoding of source files.
48 | # source_encoding = 'utf-8-sig'
49 |
50 | # The master toctree document.
51 | master_doc = 'index'
52 |
53 | # General information about the project.
54 | project = u'Django Google Drive Storage'
55 | copyright = u'2014 - {}, Gian Luca Dalla Torre'.format(
56 | datetime.datetime.now().year)
57 |
58 | # The version info for the project you're documenting, acts as replacement for
59 | # |version| and |release|, also used in various other places throughout the
60 | # built documents.
61 | #
62 | # The short X.Y version.
63 | version = '1.6.0'
64 | # The full version, including alpha/beta/rc tags.
65 | release = '1.6.0'
66 |
67 | # The language for content autogenerated by Sphinx. Refer to documentation
68 | # for a list of supported languages.
69 | # language = None
70 |
71 | # There are two options for replacing |today|: either, you set today to some
72 | # non-false value, then it is used:
73 | # today = ''
74 | # Else, today_fmt is used as the format for a strftime call.
75 | # today_fmt = '%B %d, %Y'
76 |
77 | # List of patterns, relative to source directory, that match files and
78 | # directories to ignore when looking for source files.
79 | exclude_patterns = ['_build']
80 |
81 | # The reST default role (used for this markup: `text`) to use for all
82 | # documents.
83 | # default_role = None
84 |
85 | # If true, '()' will be appended to :func: etc. cross-reference text.
86 | # add_function_parentheses = True
87 |
88 | # If true, the current module name will be prepended to all description
89 | # unit titles (such as .. function::).
90 | # add_module_names = True
91 |
92 | # If true, sectionauthor and moduleauthor directives will be shown in the
93 | # output. They are ignored by default.
94 | # show_authors = False
95 |
96 | # The name of the Pygments (syntax highlighting) style to use.
97 | pygments_style = 'sphinx'
98 |
99 | # A list of ignored prefixes for module index sorting.
100 | # modindex_common_prefix = []
101 |
102 | # If true, keep warnings as "system message" paragraphs in the built documents.
103 | # keep_warnings = False
104 |
105 |
106 | # -- Options for HTML output ----------------------------------------------
107 |
108 | # The theme to use for HTML and HTML Help pages. See the documentation for
109 | # a list of builtin themes.
110 | html_theme = 'sphinx_rtd_theme'
111 |
112 | # Theme options are theme-specific and customize the look and feel of a theme
113 | # further. For a list of options available for each theme, see the
114 | # documentation.
115 | #html_theme_options = {}
116 |
117 | # Add any paths that contain custom themes here, relative to this directory.
118 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
119 |
120 | # The name for this set of Sphinx documents. If None, it defaults to
121 | # " v documentation".
122 | # html_title = None
123 |
124 | # A shorter title for the navigation bar. Default is the same as html_title.
125 | # html_short_title = None
126 |
127 | # The name of an image file (relative to this directory) to place at the top
128 | # of the sidebar.
129 | # html_logo = None
130 |
131 | # The name of an image file (within the static path) to use as favicon of the
132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
133 | # pixels large.
134 | # html_favicon = None
135 |
136 | # Add any paths that contain custom static files (such as style sheets) here,
137 | # relative to this directory. They are copied after the builtin static files,
138 | # so a file named "default.css" will overwrite the builtin "default.css".
139 | html_static_path = ['_static']
140 |
141 | # Add any extra paths that contain custom files (such as robots.txt or
142 | # .htaccess) here, relative to this directory. These files are copied
143 | # directly to the root of the documentation.
144 | # html_extra_path = []
145 |
146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
147 | # using the given strftime format.
148 | # html_last_updated_fmt = '%b %d, %Y'
149 |
150 | # If true, SmartyPants will be used to convert quotes and dashes to
151 | # typographically correct entities.
152 | # html_use_smartypants = True
153 |
154 | # Custom sidebar templates, maps document names to template names.
155 | # html_sidebars = {}
156 |
157 | # Additional templates that should be rendered to pages, maps page names to
158 | # template names.
159 | # html_additional_pages = {}
160 |
161 | # If false, no module index is generated.
162 | # html_domain_indices = True
163 |
164 | # If false, no index is generated.
165 | # html_use_index = True
166 |
167 | # If true, the index is split into individual pages for each letter.
168 | # html_split_index = False
169 |
170 | # If true, links to the reST sources are added to the pages.
171 | # html_show_sourcelink = True
172 |
173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
174 | # html_show_sphinx = True
175 |
176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
177 | # html_show_copyright = True
178 |
179 | # If true, an OpenSearch description file will be output, and all pages will
180 | # contain a tag referring to it. The value of this option must be the
181 | # base URL from which the finished HTML is served.
182 | # html_use_opensearch = ''
183 |
184 | # This is the file name suffix for HTML files (e.g. ".xhtml").
185 | # html_file_suffix = None
186 |
187 | # Output file base name for HTML help builder.
188 | htmlhelp_basename = 'DjangoGoogleDriveStoragedoc'
189 |
190 |
191 | # -- Options for LaTeX output ---------------------------------------------
192 |
193 | latex_elements = {
194 | # The paper size ('letterpaper' or 'a4paper').
195 | # 'papersize': 'letterpaper',
196 |
197 | # The font size ('10pt', '11pt' or '12pt').
198 | # 'pointsize': '10pt',
199 |
200 | # Additional stuff for the LaTeX preamble.
201 | # 'preamble': '',
202 | }
203 |
204 | # Grouping the document tree into LaTeX files. List of tuples
205 | # (source start file, target name, title,
206 | # author, documentclass [howto, manual, or own class]).
207 | latex_documents = [
208 | ('index', 'DjangoGoogleDriveStorage.tex', u'Django Google Drive Storage Documentation',
209 | u'Gian Luca Dalla Torre', 'manual'),
210 | ]
211 |
212 | # The name of an image file (relative to this directory) to place at the top of
213 | # the title page.
214 | # latex_logo = None
215 |
216 | # For "manual" documents, if this is true, then toplevel headings are parts,
217 | # not chapters.
218 | # latex_use_parts = False
219 |
220 | # If true, show page references after internal links.
221 | # latex_show_pagerefs = False
222 |
223 | # If true, show URL addresses after external links.
224 | # latex_show_urls = False
225 |
226 | # Documents to append as an appendix to all manuals.
227 | # latex_appendices = []
228 |
229 | # If false, no module index is generated.
230 | # latex_domain_indices = True
231 |
232 |
233 | # -- Options for manual page output ---------------------------------------
234 |
235 | # One entry per manual page. List of tuples
236 | # (source start file, name, description, authors, manual section).
237 | man_pages = [
238 | ('index', 'djangogoogledrivestorage', u'Django Google Drive Storage Documentation',
239 | [u'Gian Luca Dalla Torre'], 1)
240 | ]
241 |
242 | # If true, show URL addresses after external links.
243 | # man_show_urls = False
244 |
245 |
246 | # -- Options for Texinfo output -------------------------------------------
247 |
248 | # Grouping the document tree into Texinfo files. List of tuples
249 | # (source start file, target name, title, author,
250 | # dir menu entry, description, category)
251 | texinfo_documents = [
252 | ('index', 'DjangoGoogleDriveStorage', u'Django Google Drive Storage Documentation',
253 | u'Gian Luca Dalla Torre', 'DjangoGoogleDriveStorage', 'One line description of project.',
254 | 'Miscellaneous'),
255 | ]
256 |
257 | # Documents to append as an appendix to all manuals.
258 | # texinfo_appendices = []
259 |
260 | # If false, no module index is generated.
261 | # texinfo_domain_indices = True
262 |
263 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
264 | # texinfo_show_urls = 'footnote'
265 |
266 | # If true, do not generate a @detailmenu in the "Top" node's menu.
267 | # texinfo_no_detailmenu = False
268 |
269 |
270 | # Example configuration for intersphinx: refer to the Python standard library.
271 | intersphinx_mapping = {'http://docs.python.org/': None}
272 |
--------------------------------------------------------------------------------
/gdstorage/storage.py:
--------------------------------------------------------------------------------
1 | import enum
2 | import json
3 | import mimetypes
4 | import os
5 | from io import BytesIO
6 |
7 | from dateutil.parser import parse
8 | from django.conf import settings
9 | from django.core.files import File
10 | from django.core.files.storage import Storage
11 | from django.utils.deconstruct import deconstructible
12 | from google.oauth2.service_account import Credentials
13 | from googleapiclient.discovery import build
14 | from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
15 |
16 |
17 | class GoogleDrivePermissionType(enum.Enum):
18 | """
19 | Describe a permission type for Google Drive as described on
20 | `Drive docs `_
21 | """ # noqa: E501
22 |
23 | USER = 'user' # Permission for single user
24 |
25 | GROUP = 'group' # Permission for group defined in Google Drive
26 |
27 | DOMAIN = 'domain' # Permission for domain defined in Google Drive
28 |
29 | ANYONE = 'anyone' # Permission for anyone
30 |
31 |
32 | class GoogleDrivePermissionRole(enum.Enum):
33 | """
34 | Describe a permission role for Google Drive as described on
35 | `Drive docs `_
36 | """ # noqa: E501
37 |
38 | OWNER = 'owner' # File Owner
39 |
40 | READER = 'reader' # User can read a file
41 |
42 | WRITER = 'writer' # User can write a file
43 |
44 | COMMENTER = 'commenter' # User can comment a file
45 |
46 |
47 | @deconstructible
48 | class GoogleDriveFilePermission(object):
49 | """
50 | Describe a permission for Google Drive as described on
51 | `Drive docs `_
52 |
53 | :param gdstorage.GoogleDrivePermissionRole g_role: Role associated to this permission
54 | :param gdstorage.GoogleDrivePermissionType g_type: Type associated to this permission
55 | :param str g_value: email address that qualifies the User associated to this permission
56 |
57 | """ # noqa: E501
58 |
59 | @property
60 | def role(self):
61 | """
62 | Role associated to this permission
63 |
64 | :return: Enumeration that states the role associated to this permission
65 | :rtype: gdstorage.GoogleDrivePermissionRole
66 | """
67 | return self._role
68 |
69 | @property
70 | def type(self):
71 | """
72 | Type associated to this permission
73 |
74 | :return: Enumeration that states the role associated to this permission
75 | :rtype: gdstorage.GoogleDrivePermissionType
76 | """
77 | return self._type
78 |
79 | @property
80 | def value(self):
81 | """
82 | Email that qualifies the user associated to this permission
83 | :return: Email as string
84 | :rtype: str
85 | """
86 | return self._value
87 |
88 | @property
89 | def raw(self):
90 | """
91 | Transform the :class:`.GoogleDriveFilePermission` instance into a
92 | string used to issue the command to Google Drive API
93 |
94 | :return: Dictionary that states a permission compliant with Google Drive API
95 | :rtype: dict
96 | """ # noqa: E501
97 |
98 | result = {
99 | 'role': self.role.value,
100 | 'type': self.type.value
101 | }
102 |
103 | if self.value is not None:
104 | result['emailAddress'] = self.value
105 |
106 | return result
107 |
108 | def __init__(self, g_role, g_type, g_value=None):
109 | """
110 | Instantiate this class
111 | """
112 | if not isinstance(g_role, GoogleDrivePermissionRole):
113 | raise ValueError(
114 | 'Role should be a GoogleDrivePermissionRole instance'
115 | )
116 | if not isinstance(g_type, GoogleDrivePermissionType):
117 | raise ValueError(
118 | 'Permission should be a GoogleDrivePermissionType instance'
119 | )
120 | if g_value is not None and not isinstance(g_value, str):
121 | raise ValueError('Value should be a String instance')
122 |
123 | self._role = g_role
124 | self._type = g_type
125 | self._value = g_value
126 |
127 |
128 | _ANYONE_CAN_READ_PERMISSION_ = GoogleDriveFilePermission(
129 | GoogleDrivePermissionRole.READER,
130 | GoogleDrivePermissionType.ANYONE
131 | )
132 |
133 |
134 | @deconstructible
135 | class GoogleDriveStorage(Storage):
136 | """
137 | Storage class for Django that interacts with Google Drive as persistent
138 | storage.
139 | This class uses a system account for Google API that create an
140 | application drive (the drive is not owned by any Google User, but it is
141 | owned by the application declared on Google API console).
142 | """
143 |
144 | _UNKNOWN_MIMETYPE_ = 'application/octet-stream'
145 | _GOOGLE_DRIVE_FOLDER_MIMETYPE_ = 'application/vnd.google-apps.folder'
146 | KEY_FILE_PATH = 'GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE'
147 | KEY_FILE_CONTENT = 'GOOGLE_DRIVE_STORAGE_JSON_KEY_FILE_CONTENTS'
148 |
149 | def __init__(self, json_keyfile_path=None, permissions=None):
150 | """
151 | Handles credentials and builds the google service.
152 |
153 | :param json_keyfile_path: Path
154 | :raise ValueError:
155 | """
156 | settings_keyfile_path = getattr(settings, self.KEY_FILE_PATH, None)
157 | self._json_keyfile_path = json_keyfile_path or settings_keyfile_path
158 |
159 | if self._json_keyfile_path:
160 | credentials = Credentials.from_service_account_file(
161 | self._json_keyfile_path,
162 | scopes=['https://www.googleapis.com/auth/drive'],
163 | )
164 | else:
165 | credentials = Credentials.from_service_account_info(
166 | json.loads(os.environ[self.KEY_FILE_CONTENT]),
167 | scopes=['https://www.googleapis.com/auth/drive'],
168 | )
169 |
170 | self._permissions = None
171 | if permissions is None:
172 | self._permissions = (_ANYONE_CAN_READ_PERMISSION_,)
173 | else:
174 | if not isinstance(permissions, (tuple, list,)):
175 | raise ValueError(
176 | 'Permissions should be a list or a tuple of '
177 | 'GoogleDriveFilePermission instances'
178 | )
179 | else:
180 | for p in permissions:
181 | if not isinstance(p, GoogleDriveFilePermission):
182 | raise ValueError(
183 | 'Permissions should be a list or a tuple of '
184 | 'GoogleDriveFilePermission instances'
185 | )
186 | # Ok, permissions are good
187 | self._permissions = permissions
188 |
189 | self._drive_service = build('drive', 'v3', credentials=credentials)
190 |
191 | def _split_path(self, p):
192 | """
193 | Split a complete path in a list of strings
194 |
195 | :param p: Path to be splitted
196 | :type p: string
197 | :returns: list - List of strings that composes the path
198 | """
199 | p = p[1:] if p[0] == '/' else p
200 | a, b = os.path.split(p)
201 | return (self._split_path(a) if len(a) and len(b) else []) + [b]
202 |
203 | def _get_or_create_folder(self, path, parent_id=None):
204 | """
205 | Create a folder on Google Drive.
206 | It creates folders recursively.
207 | If the folder already exists, it retrieves only the unique identifier.
208 |
209 | :param path: Path that had to be created
210 | :type path: string
211 | :param parent_id: Unique identifier for its parent (folder)
212 | :type parent_id: string
213 | :returns: dict
214 | """
215 | folder_data = self._check_file_exists(path, parent_id)
216 | if folder_data is not None:
217 | return folder_data
218 |
219 | # Folder does not exists, have to create
220 | split_path = self._split_path(path)
221 |
222 | if split_path[:-1]:
223 | parent_path = os.path.join(*split_path[:-1])
224 | current_folder_data = self._get_or_create_folder(
225 | parent_path, parent_id=parent_id
226 | )
227 | else:
228 | current_folder_data = None
229 |
230 | meta_data = {
231 | 'name': split_path[-1],
232 | 'mimeType': self._GOOGLE_DRIVE_FOLDER_MIMETYPE_
233 | }
234 | if current_folder_data is not None:
235 | meta_data['parents'] = [current_folder_data['id']]
236 | else:
237 | # This is the first iteration loop so we have to set
238 | # the parent_id obtained by the user, if available
239 | if parent_id is not None:
240 | meta_data['parents'] = [parent_id]
241 | current_folder_data = self._drive_service.files().create(
242 | body=meta_data).execute()
243 | return current_folder_data
244 |
245 | def _check_file_exists(self, filename, parent_id=None):
246 | """
247 | Check if a file with specific parameters exists in Google Drive.
248 | :param filename: File or folder to search
249 | :type filename: string
250 | :param parent_id: Unique identifier for its parent (folder)
251 | :type parent_id: string
252 | :returns: dict containing file / folder data if exists or None if does not exists
253 | """ # noqa: E501
254 | if len(filename) == 0:
255 | # This is the lack of directory at the beginning of a 'file.txt'
256 | # Since the target file lacks directories, the assumption
257 | # is that it belongs at '/'
258 | return self._drive_service.files().get(fileId='root').execute()
259 | split_filename = self._split_path(filename)
260 | if len(split_filename) > 1:
261 | # This is an absolute path with folder inside
262 | # First check if the first element exists as a folder
263 | # If so call the method recursively with next portion of path
264 | # Otherwise the path does not exists hence
265 | # the file does not exists
266 | q = "mimeType = '{0}' and name = '{1}'".format(
267 | self._GOOGLE_DRIVE_FOLDER_MIMETYPE_, split_filename[0],
268 | )
269 | if parent_id is not None:
270 | q = "{0} and '{1}' in parents".format(q, parent_id)
271 | results = self._drive_service.files().list(
272 | q=q, fields='nextPageToken, files(*)').execute()
273 | items = results.get('files', [])
274 | for item in items:
275 | if item['name'] == split_filename[0]:
276 | # Assuming every folder has a single parent
277 | return self._check_file_exists(
278 | os.path.sep.join(split_filename[1:]), item['id'])
279 | return None
280 | # This is a file, checking if exists
281 | q = "name = '{0}'".format(split_filename[0])
282 | if parent_id is not None:
283 | q = "{0} and '{1}' in parents".format(q, parent_id)
284 | results = self._drive_service.files().list(
285 | q=q, fields='nextPageToken, files(*)').execute()
286 | items = results.get('files', [])
287 | if len(items) > 0:
288 | return items[0]
289 | q = '' if parent_id is None else "'{0}' in parents".format(parent_id)
290 | results = self._drive_service.files().list(
291 | q=q, fields='nextPageToken, files(*)').execute()
292 | items = results.get('files', [])
293 | for item in items:
294 | if split_filename[0] in item['name']:
295 | return item
296 | return None
297 |
298 | # Methods that had to be implemented
299 | # to create a valid storage for Django
300 |
301 | def _open(self, name, mode='rb'):
302 | """For more details see
303 | https://developers.google.com/drive/api/v3/manage-downloads?hl=id#download_a_file_stored_on_google_drive
304 | """ # noqa: E501
305 | file_data = self._check_file_exists(name)
306 | request = self._drive_service.files().get_media(
307 | fileId=file_data['id'])
308 | fh = BytesIO()
309 | downloader = MediaIoBaseDownload(fh, request)
310 | done = False
311 | while done is False:
312 | _, done = downloader.next_chunk()
313 | fh.seek(0)
314 | return File(fh, name)
315 |
316 | def _save(self, name, content):
317 | name = os.path.join(settings.GOOGLE_DRIVE_STORAGE_MEDIA_ROOT, name)
318 | folder_path = os.path.sep.join(self._split_path(name)[:-1])
319 | folder_data = self._get_or_create_folder(folder_path)
320 | parent_id = None if folder_data is None else folder_data['id']
321 | # Now we had created (or obtained) folder on GDrive
322 | # Upload the file
323 | mime_type, _ = mimetypes.guess_type(name)
324 | if mime_type is None:
325 | mime_type = self._UNKNOWN_MIMETYPE_
326 | media_body = MediaIoBaseUpload(
327 | content.file, mime_type, resumable=True, chunksize=1024 * 512)
328 | body = {
329 | 'name': self._split_path(name)[-1],
330 | 'mimeType': mime_type
331 | }
332 | # Set the parent folder.
333 | if parent_id:
334 | body['parents'] = [parent_id]
335 | file_data = self._drive_service.files().create(
336 | body=body,
337 | media_body=media_body).execute()
338 |
339 | # Setting up permissions
340 | for p in self._permissions:
341 | self._drive_service.permissions().create(
342 | fileId=file_data['id'], body={**p.raw}).execute()
343 |
344 | return file_data.get('originalFilename', file_data.get('name'))
345 |
346 | def delete(self, name):
347 | """
348 | Deletes the specified file from the storage system.
349 | """
350 | file_data = self._check_file_exists(name)
351 | if file_data is not None:
352 | self._drive_service.files().delete(
353 | fileId=file_data['id']).execute()
354 |
355 | def exists(self, name):
356 | """
357 | Returns True if a file referenced by the given name already exists
358 | in the storage system, or False if the name is available for
359 | a new file.
360 | """
361 | return self._check_file_exists(name) is not None
362 |
363 | def listdir(self, path):
364 | """
365 | Lists the contents of the specified path, returning a 2-tuple of lists;
366 | the first item being directories, the second item being files.
367 | """
368 | directories, files = [], []
369 | if path == '/':
370 | folder_id = {'id': 'root'}
371 | else:
372 | folder_id = self._check_file_exists(path)
373 | if folder_id:
374 | file_params = {
375 | 'q': "'{0}' in parents and mimeType != '{1}'".format(
376 | folder_id['id'], self._GOOGLE_DRIVE_FOLDER_MIMETYPE_),
377 | }
378 | dir_params = {
379 | 'q': "'{0}' in parents and mimeType = '{1}'".format(
380 | folder_id['id'], self._GOOGLE_DRIVE_FOLDER_MIMETYPE_),
381 | }
382 | files_results = self._drive_service.files().list(**file_params).execute() # noqa: E501
383 | dir_results = self._drive_service.files().list(**dir_params).execute() # noqa: E501
384 | files_list = files_results.get('files', [])
385 | dir_list = dir_results.get('files', [])
386 | for element in files_list:
387 | files.append(os.path.join(path, element['name']))
388 | for element in dir_list:
389 | directories.append(os.path.join(path, element['name']))
390 | return directories, files
391 |
392 | def size(self, name):
393 | """
394 | Returns the total size, in bytes, of the file specified by name.
395 | """
396 | file_data = self._check_file_exists(name)
397 | if file_data is None:
398 | return 0
399 | return file_data['size']
400 |
401 | def url(self, name):
402 | """
403 | Returns an absolute URL where the file's contents can be accessed
404 | directly by a Web browser.
405 | """
406 | file_data = self._check_file_exists(name)
407 | if file_data is None:
408 | return None
409 | return file_data['webContentLink']
410 |
411 | def accessed_time(self, name):
412 | """
413 | Returns the last accessed time (as datetime object) of the file
414 | specified by name.
415 | """
416 | return self.modified_time(name)
417 |
418 | def created_time(self, name):
419 | """
420 | Returns the creation time (as datetime object) of the file
421 | specified by name.
422 | """
423 | file_data = self._check_file_exists(name)
424 | if file_data is None:
425 | return None
426 | return parse(file_data['createdDate'])
427 |
428 | def modified_time(self, name):
429 | """
430 | Returns the last modified time (as datetime object) of the file
431 | specified by name.
432 | """
433 | file_data = self._check_file_exists(name)
434 | if file_data is None:
435 | return None
436 | return parse(file_data['modifiedDate'])
437 |
438 | def deconstruct(self):
439 | """
440 | Handle field serialization to support migration
441 | """
442 | name, path, args, kwargs = super().deconstruct()
443 | if self._service_email is not None:
444 | kwargs['service_email'] = self._service_email
445 | if self._json_keyfile_path is not None:
446 | kwargs['json_keyfile_path'] = self._json_keyfile_path
447 | return name, path, args, kwargs
448 |
--------------------------------------------------------------------------------