Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ab5dcff
Add default value for url expiring time
jxltom Mar 27, 2018
8ecf260
Merge branch 'fix-url-function'
jxltom Mar 27, 2018
f0e81a7
Change default expires to 30 days
jxltom Mar 27, 2018
ac7c039
Merge branch 'fix-url-function'
jxltom Mar 27, 2018
d9a31ec
Append prefix with / when it is considered as a directory
jxltom Mar 27, 2018
ff2699a
Merge branch 'fix-collectstatic'
jxltom Mar 27, 2018
4d4b9c5
Remove positional argument expire
jxltom Mar 28, 2018
bd71253
Remove expire argument in url function in tests
jxltom Mar 28, 2018
6b256ab
Do not return signed url for pubic or pubic-read bucket
jxltom Mar 28, 2018
b64f9c6
Add OSS_EXPIRE_TIME env var
jxltom Mar 28, 2018
a79cfe9
Merge branch 'fix-url-function'
jxltom Mar 28, 2018
09fc9d8
Fixed typo
jxltom Mar 28, 2018
d1425fb
Merge branch 'fix-url-function'
jxltom Mar 28, 2018
bffa4ed
Update README
jxltom Mar 28, 2018
3558926
Merge branch 'fix-url-function'
jxltom Mar 28, 2018
403b22c
Revert "Append prefix with / when it is considered as a directory"
jxltom Mar 27, 2018
6a458b1
Add / back since os.path.normpath will remove / at the end of path in…
jxltom Mar 28, 2018
df66519
Merge branch 'fix-collectstatic'
jxltom Mar 28, 2018
e9ced64
Remove left / in name otherwise urljoin won't work
jxltom May 15, 2018
116e570
Merge branch 'fix-url-function'
jxltom May 15, 2018
a7b6d48
Fixed MEDIA_URL to work with Aliyun CDN.
favoyang May 15, 2018
1741da9
Fixed STATIC_URL to work with Aliyun CDN.
favoyang May 16, 2018
363c4e9
Bug fix for serving static files via MEDIA_URL, and improved document.
favoyang May 26, 2018
14afd81
Fixed backward compatibility.
favoyang May 26, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Django AliCloud OSS Storage
=========================
===========================

**django-oss-storage** provides a Django AliCloud OSS file storage.

Expand All @@ -9,6 +9,7 @@ Features

- Django file storage for AliCloud OSS
- Django static file storage for AliCloud OSS
- Serving file via AliCloud CDN
- Works in Python 2 & 3

Installation
Expand Down Expand Up @@ -46,8 +47,8 @@ Use the following settings to authenticate with AliCloud OSS.
# AliCloud access key secret
OSS_ACCESS_KEY_SECRET = <Your Access Key Secret>

File storage settings
=====================
OSS storage settings
====================

Use the following settings to configure AliCloud OSS file storage.

Expand All @@ -60,21 +61,47 @@ Use the following settings to configure AliCloud OSS file storage.
# Refer https://www.alibabacloud.com/help/zh/doc-detail/31837.htm for OSS Region & Endpoint
OSS_ENDPOINT = <Your access endpoint>

# The default location for your files
MEDIA_URL = '/media/'
# The expire time to construct signed url for private acl bucket.
# Can be set by OSS_EXPIRE_TIME as environment variable or as Django
# settings. The default value is 30 days.
OSS_EXPIRE_TIME = <Expire Time in Seconds>

OSS media storage settings
==========================

All of the OSS storage settings are available for the media storage.

.. code-block:: bash

# The default location for the media files stored in bucket.
OSS_MEDIA_LOCATION = '/media/'

Staticfiles storage settings
============================
# URL that handles the media served. It only works for public or
# public-read acl bucket (i.e. put AliCloud OSS behind CDN).
# If value not starts with 'http', storage urls will fallback to
# default OSS url which is bucket_name.endpoint/key format.
# For private acl, storage urls will be the signed url.
MEDIA_URL = 'https://media.example.com/'

All of the file storage settings are available for the staticfiles storage.
OSS static storage storage settings
===================================

All of the OSS storage settings are available for the media storage.

.. code-block:: bash

# The default location for your static files
STATIC_URL = '/static/'
# The default location for the static files stored in bucket.
OSS_STATIC_LOCATION = '/static/'

# URL that handles the static file served. It only works for public or
# public-read acl bucket (i.e. put AliCloud OSS behind CDN).
# If value not starts with 'http', storage urls will fallback to
# default OSS url which is bucket_name.endpoint/key format.
# For private acl, storage urls will be the signed url.
STATIC_URL = 'https://static.example.com/'

staticfiles provides command 'collectstatic'. Run following command to collect all sub-folder 'static' of each app
and upload to STATIC_URL.
and upload to OSS_STATIC_LOCATION.

.. code-block:: bash

Expand Down
54 changes: 41 additions & 13 deletions django_oss_storage/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@

import oss2.utils
import oss2.exceptions
from oss2 import Auth, Service, Bucket, ObjectIterator
from oss2 import Auth, Service, Bucket, ObjectIterator, BUCKET_ACL_PRIVATE

from .defaults import logger


def _get_config(name):
config = os.environ.get(name, getattr(settings, name, None))
def _get_config(name, default=None):
config = os.environ.get(name, getattr(settings, name, default))
if config is not None:
if isinstance(config, six.string_types):
return config.strip()
Expand Down Expand Up @@ -56,19 +56,22 @@ class OssStorage(Storage):
Aliyun OSS Storage
"""

def __init__(self, access_key_id=None, access_key_secret=None, end_point=None, bucket_name=None):
def __init__(self, access_key_id=None, access_key_secret=None, end_point=None, bucket_name=None, expire_time=None, location='', base_url=''):
self.access_key_id = access_key_id if access_key_id else _get_config('OSS_ACCESS_KEY_ID')
self.access_key_secret = access_key_secret if access_key_secret else _get_config('OSS_ACCESS_KEY_SECRET')
self.end_point = _normalize_endpoint(end_point if end_point else _get_config('OSS_ENDPOINT'))
self.bucket_name = bucket_name if bucket_name else _get_config('OSS_BUCKET_NAME')
self.expire_time = expire_time if expire_time else int(_get_config('OSS_EXPIRE_TIME', default=60*60*24*30))

self.auth = Auth(self.access_key_id, self.access_key_secret)
self.service = Service(self.auth, self.end_point)
self.bucket = Bucket(self.auth, self.end_point, self.bucket_name)
self.location = location
self.base_url = base_url

# try to get bucket acl to check bucket exist or not
try:
self.bucket.get_bucket_acl().acl
self.bucket_acl = self.bucket.get_bucket_acl().acl
except oss2.exceptions.NoSuchBucket:
raise SuspiciousOperation("Bucket '%s' does not exist." % self.bucket_name)

Expand All @@ -79,10 +82,17 @@ def _get_key_name(self, name):
input : test.txt
output : media/test.txt
"""
# urljoin won't work if name is absolute path
name = name.lstrip('/')

base_path = force_text(self.location)
final_path = urljoin(base_path + "/", name)
name = os.path.normpath(final_path.lstrip('/'))

# Add / to the end of path since os.path.normpath will remove it
if final_path.endswith('/') and not name.endswith('/'):
name += '/'

if six.PY2:
name = name.encode('utf-8')
return name
Expand Down Expand Up @@ -197,9 +207,20 @@ def listdir(self, name):
logger().debug("files: %s", files)
return dirs, files

def url(self, name, expire):
def url(self, name):
key = self._get_key_name(name)
return self.bucket.sign_url('GET', key, expire)

# Return signed bucket url for private acl.
if self.bucket_acl == BUCKET_ACL_PRIVATE:
return self.bucket.sign_url('GET', key, expires=self.expire_time)

# For public or public-read acl bucket, use base_url is possible,
# otherwise fallback to public bucket url.
if self.base_url.startswith("http"):
return urljoin(self.base_url, key)
else:
scheme, endpoint = self.end_point.split('//')
return urljoin(scheme + '//' + self.bucket_name + '.' + endpoint, key)

def delete(self, name):
name = self._get_key_name(name)
Expand All @@ -215,19 +236,26 @@ def delete_with_slash(self, dirname):

class OssMediaStorage(OssStorage):
def __init__(self):
self.location = settings.MEDIA_URL
logger().debug("locatin: %s", self.location)
super(OssMediaStorage, self).__init__()
fallback_location = settings.MEDIA_URL
if fallback_location.startswith('http'):
fallback_location = '/media/'
super(OssMediaStorage, self).__init__(
location=getattr(settings, 'OSS_MEDIA_LOCATION', fallback_location),
base_url=settings.MEDIA_URL)

def save(self, name, content, max_length=None):
return super(OssMediaStorage, self)._save(name, content)


class OssStaticStorage(OssStorage):
def __init__(self):
self.location = settings.STATIC_URL
logger().info("locatin: %s", self.location)
super(OssStaticStorage, self).__init__()
fallback_location = settings.STATIC_URL
if fallback_location.startswith('http'):
fallback_location = '/static/'
super(OssStaticStorage, self).__init__(
location=getattr(settings, 'OSS_STATIC_LOCATION', fallback_location),
base_url=settings.STATIC_URL
)

def save(self, name, content, max_length=None):
return super(OssStaticStorage, self)._save(name, content)
Expand Down
12 changes: 6 additions & 6 deletions tests/django-oss-storage-test/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@ def test_save_small_file(self):
with self.save_file():
logging.info("content type: %s", default_storage.content_type("test.txt"))
self.assertEqual(default_storage.open("test.txt").read(), b"test")
self.assertEqual(requests.get(default_storage.url("test.txt", 60)).content, b"test")
self.assertEqual(requests.get(default_storage.url("test.txt")).content, b"test")

def test_save_big_file(self):
with self.save_file(content=b"test" * 1000):
logging.info("content type: %s", default_storage.content_type("test.txt"))
self.assertEqual(default_storage.open("test.txt").read(), b"test" * 1000)
self.assertEqual(requests.get(default_storage.url("test.txt", 60)).content, b"test" * 1000)
self.assertEqual(requests.get(default_storage.url("test.txt")).content, b"test" * 1000)

def test_url(self):
with self.save_file():
url = default_storage.url("test.txt", 100)
url = default_storage.url("test.txt")
logging.info("url: %s", url)
response = requests.get(url)

Expand All @@ -110,7 +110,7 @@ def test_url_cn(self):
logging.info("objname: %s", objname)
with self.save_file(objname, content=u'我的座右铭') as name:
self.assertEqual(name, objname)
url = default_storage.url(objname, 300)
url = default_storage.url(objname)
logging.info("url: %s", url)
response = requests.get(url)
self.assertEqual(response.status_code, 200)
Expand Down Expand Up @@ -229,7 +229,7 @@ def test_overwrite_cn(self):

def test_static_url(self):
with self.save_file(storage=staticfiles_storage):
url = staticfiles_storage.url("test.txt", 60)
url = staticfiles_storage.url("test.txt")
logging.info("url: %s", url)
response = requests.get(url)

Expand All @@ -239,7 +239,7 @@ def test_static_url(self):

def test_configured_url(self):
with self.settings(MEDIA_URL= "/media/"), self.save_file():
url = default_storage.url("test.txt", 60)
url = default_storage.url("test.txt")
logging.info("url: %s", url)
response = requests.get(url)

Expand Down