summaryrefslogtreecommitdiff
path: root/storages/backends
diff options
context:
space:
mode:
authorYuval Adam <yuval@segmanta.com>2014-03-20 17:49:16 +0200
committerYuval Adam <yuval@segmanta.com>2014-03-20 17:49:16 +0200
commit75f93a3f7854a7383febd047391e2bfd4caceee0 (patch)
treed5e634ef292c6b84acacc3982dbcaa0d300a346e /storages/backends
Initial git fork of django-storagesHEADmaster
Diffstat (limited to 'storages/backends')
-rw-r--r--storages/backends/__init__.py0
-rw-r--r--storages/backends/apache_libcloud.py180
-rw-r--r--storages/backends/azure_storage.py65
-rw-r--r--storages/backends/couchdb.py135
-rw-r--r--storages/backends/database.py132
-rw-r--r--storages/backends/ftp.py261
-rw-r--r--storages/backends/gs.py99
-rw-r--r--storages/backends/hashpath.py40
-rw-r--r--storages/backends/image.py55
-rw-r--r--storages/backends/mogile.py118
-rw-r--r--storages/backends/mongodb.py104
-rw-r--r--storages/backends/mosso.py347
-rw-r--r--storages/backends/overwrite.py19
-rw-r--r--storages/backends/s3.py288
-rw-r--r--storages/backends/s3boto.py501
-rw-r--r--storages/backends/sftpstorage.py272
-rw-r--r--storages/backends/symlinkorcopy.py62
17 files changed, 2678 insertions, 0 deletions
diff --git a/storages/backends/__init__.py b/storages/backends/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/storages/backends/__init__.py
diff --git a/storages/backends/apache_libcloud.py b/storages/backends/apache_libcloud.py
new file mode 100644
index 0000000..a350d65
--- /dev/null
+++ b/storages/backends/apache_libcloud.py
@@ -0,0 +1,180 @@
+# Django storage using libcloud providers
+# Aymeric Barantal (mric at chamal.fr) 2011
+#
+import os
+
+from django.conf import settings
+from django.core.files.storage import Storage
+from django.core.files.base import File
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+try:
+ from libcloud.storage.providers import get_driver
+ from libcloud.storage.types import ObjectDoesNotExistError, Provider
+except ImportError:
+ raise ImproperlyConfigured("Could not load libcloud")
+
+
+class LibCloudStorage(Storage):
+ """Django storage derived class using apache libcloud to operate
+ on supported providers"""
+ def __init__(self, provider_name=None, option=None):
+ if provider_name is None:
+ provider_name = getattr(settings, 'DEFAULT_LIBCLOUD_PROVIDER', 'default')
+
+ self.provider = settings.LIBCLOUD_PROVIDERS.get(provider_name)
+ if not self.provider:
+ raise ImproperlyConfigured(
+ 'LIBCLOUD_PROVIDERS %s not defined or invalid' % provider_name)
+ extra_kwargs = {}
+ if 'region' in self.provider:
+ extra_kwargs['region'] = self.provider['region']
+ try:
+ provider_type = self.provider['type']
+ if isinstance(provider_type, basestring):
+ module_path, tag = provider_type.rsplit('.', 1)
+ if module_path != 'libcloud.storage.types.Provider':
+ raise ValueError("Invalid module path")
+ provider_type = getattr(Provider, tag)
+
+ Driver = get_driver(provider_type)
+ self.driver = Driver(
+ self.provider['user'],
+ self.provider['key'],
+ **extra_kwargs
+ )
+ except Exception as e:
+ raise ImproperlyConfigured(
+ "Unable to create libcloud driver type %s: %s" % \
+ (self.provider.get('type'), e))
+ self.bucket = self.provider['bucket'] # Limit to one container
+
+ def _get_bucket(self):
+ """Helper to get bucket object (libcloud container)"""
+ return self.driver.get_container(self.bucket)
+
+ def _clean_name(self, name):
+ """Clean name (windows directories)"""
+ return os.path.normpath(name).replace('\\', '/')
+
+ def _get_object(self, name):
+ """Get object by its name. Return None if object not found"""
+ clean_name = self._clean_name(name)
+ try:
+ return self.driver.get_object(self.bucket, clean_name)
+ except ObjectDoesNotExistError:
+ return None
+
+ def delete(self, name):
+ """Delete object on remote"""
+ obj = self._get_object(name)
+ if obj:
+ return self.driver.delete_object(obj)
+ else:
+ raise Exception('Object to delete does not exists')
+
+ def exists(self, name):
+ obj = self._get_object(name)
+ return True if obj else False
+
+ def listdir(self, path='/'):
+ """Lists the contents of the specified path,
+ returning a 2-tuple of lists; the first item being
+ directories, the second item being files.
+ """
+ container = self._get_bucket()
+ objects = self.driver.list_container_objects(container)
+ path = self._clean_name(path)
+ if not path.endswith('/'):
+ path = "%s/" % path
+ files = []
+ dirs = []
+ # TOFIX: better algorithm to filter correctly
+ # (and not depend on google-storage empty folder naming)
+ for o in objects:
+ if path == '/':
+ if o.name.count('/') == 0:
+ files.append(o.name)
+ elif o.name.count('/') == 1:
+ dir_name = o.name[:o.name.index('/')]
+ if not dir_name in dirs:
+ dirs.append(dir_name)
+ elif o.name.startswith(path):
+ if o.name.count('/') <= path.count('/'):
+ # TOFIX : special case for google storage with empty dir
+ if o.name.endswith('_$folder$'):
+ name = o.name[:-9]
+ name = name[len(path):]
+ dirs.append(name)
+ else:
+ name = o.name[len(path):]
+ files.append(name)
+ return (dirs, files)
+
+ def size(self, name):
+ obj = self._get_object(name)
+ if obj:
+ return obj.size
+ else:
+ return -1
+
+ def url(self, name):
+ obj = self._get_object(name)
+ return self.driver.get_object_cdn_url(obj)
+
+ def _open(self, name, mode='rb'):
+ remote_file = LibCloudFile(name, self, mode=mode)
+ return remote_file
+
+ def _read(self, name, start_range=None, end_range=None):
+ obj = self._get_object(name)
+ # TOFIX : we should be able to read chunk by chunk
+ return next(self.driver.download_object_as_stream(obj, obj.size))
+
+ def _save(self, name, file):
+ self.driver.upload_object_via_stream(iter(file), self._get_bucket(), name)
+ return name
+
+
+class LibCloudFile(File):
+ """File inherited class for libcloud storage objects read and write"""
+ def __init__(self, name, storage, mode):
+ self._name = name
+ self._storage = storage
+ self._mode = mode
+ self._is_dirty = False
+ self.file = StringIO()
+ self.start_range = 0
+
+ @property
+ def size(self):
+ if not hasattr(self, '_size'):
+ self._size = self._storage.size(self._name)
+ return self._size
+
+ def read(self, num_bytes=None):
+ if num_bytes is None:
+ args = []
+ self.start_range = 0
+ else:
+ args = [self.start_range, self.start_range + num_bytes - 1]
+ data = self._storage._read(self._name, *args)
+ self.file = StringIO(data)
+ return self.file.getvalue()
+
+ def write(self, content):
+ if 'w' not in self._mode:
+ raise AttributeError("File was opened for read-only access.")
+ self.file = StringIO(content)
+ self._is_dirty = True
+
+ def close(self):
+ if self._is_dirty:
+ self._storage._save(self._name, self.file)
+ self.file.close()
diff --git a/storages/backends/azure_storage.py b/storages/backends/azure_storage.py
new file mode 100644
index 0000000..96ef082
--- /dev/null
+++ b/storages/backends/azure_storage.py
@@ -0,0 +1,65 @@
+import os.path
+
+from django.core.files.base import ContentFile
+from django.core.files.storage import Storage
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ import azure
+ import azure.storage
+except ImportError:
+ raise ImproperlyConfigured(
+ "Could not load Azure bindings. "
+ "See https://github.com/WindowsAzure/azure-sdk-for-python")
+
+from storages.utils import setting
+
+
+def clean_name(name):
+ return os.path.normpath(name).replace("\\", "/")
+
+
+class AzureStorage(Storage):
+ account_name = setting("AZURE_ACCOUNT_NAME")
+ account_key = setting("AZURE_ACCOUNT_KEY")
+ azure_container = setting("AZURE_CONTAINER")
+
+ def __init__(self, *args, **kwargs):
+ super(AzureStorage, self).__init__(*args, **kwargs)
+ self._connection = None
+
+ @property
+ def connection(self):
+ if self._connection is None:
+ self._connection = azure.storage.BlobService(
+ self.account_name, self.account_key)
+ return self._connection
+
+ def _open(self, name, mode="rb"):
+ contents = self.connection.get_blob(self.azure_container, name)
+ return ContentFile(contents)
+
+ def exists(self, name):
+ try:
+ self.connection.get_blob_properties(
+ self.azure_container, name)
+ except azure.WindowsAzureMissingResourceError:
+ return False
+ else:
+ return True
+
+ def delete(self, name):
+ self.connection.delete_blob(self.azure_container, name)
+
+ def size(self, name):
+ properties = self.connection.get_blob_properties(
+ self.azure_container, name)
+ return properties["content-length"]
+
+ def _save(self, name, content):
+ self.connection.put_blob(self.azure_container, name,
+ content, "BlockBlob")
+ return name
+
+ def url(self, name):
+ return "%s/%s" % (self.azure_bucket, name)
diff --git a/storages/backends/couchdb.py b/storages/backends/couchdb.py
new file mode 100644
index 0000000..1e116da
--- /dev/null
+++ b/storages/backends/couchdb.py
@@ -0,0 +1,135 @@
+"""
+This is a Custom Storage System for Django with CouchDB backend.
+Created by Christian Klein.
+(c) Copyright 2009 HUDORA GmbH. All Rights Reserved.
+"""
+import os
+from cStringIO import StringIO
+from urlparse import urljoin
+from urllib import quote_plus
+
+from django.conf import settings
+from django.core.files import File
+from django.core.files.storage import Storage
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ import couchdb
+except ImportError:
+ raise ImproperlyConfigured("Could not load couchdb dependency.\
+ \nSee http://code.google.com/p/couchdb-python/")
+
+DEFAULT_SERVER= getattr(settings, 'COUCHDB_DEFAULT_SERVER', 'http://couchdb.local:5984')
+STORAGE_OPTIONS= getattr(settings, 'COUCHDB_STORAGE_OPTIONS', {})
+
+
+class CouchDBStorage(Storage):
+ """
+ CouchDBStorage - a Django Storage class for CouchDB.
+
+ The CouchDBStorage can be configured in settings.py, e.g.::
+
+ COUCHDB_STORAGE_OPTIONS = {
+ 'server': "http://example.org",
+ 'database': 'database_name'
+ }
+
+ Alternatively, the configuration can be passed as a dictionary.
+ """
+ def __init__(self, **kwargs):
+ kwargs.update(STORAGE_OPTIONS)
+ self.base_url = kwargs.get('server', DEFAULT_SERVER)
+ server = couchdb.client.Server(self.base_url)
+ self.db = server[kwargs.get('database')]
+
+ def _put_file(self, name, content):
+ self.db[name] = {'size': len(content)}
+ self.db.put_attachment(self.db[name], content, filename='content')
+ return name
+
+ def get_document(self, name):
+ return self.db.get(name)
+
+ def _open(self, name, mode='rb'):
+ couchdb_file = CouchDBFile(name, self, mode=mode)
+ return couchdb_file
+
+ def _save(self, name, content):
+ content.open()
+ if hasattr(content, 'chunks'):
+ content_str = ''.join(chunk for chunk in content.chunks())
+ else:
+ content_str = content.read()
+ name = name.replace('/', '-')
+ return self._put_file(name, content_str)
+
+ def exists(self, name):
+ return name in self.db
+
+ def size(self, name):
+ doc = self.get_document(name)
+ if doc:
+ return doc['size']
+ return 0
+
+ def url(self, name):
+ return urljoin(self.base_url,
+ os.path.join(quote_plus(self.db.name),
+ quote_plus(name),
+ 'content'))
+
+ def delete(self, name):
+ try:
+ del self.db[name]
+ except couchdb.client.ResourceNotFound:
+ raise IOError("File not found: %s" % name)
+
+ #def listdir(self, name):
+ # _all_docs?
+ # pass
+
+
+class CouchDBFile(File):
+ """
+ CouchDBFile - a Django File-like class for CouchDB documents.
+ """
+
+ def __init__(self, name, storage, mode):
+ self._name = name
+ self._storage = storage
+ self._mode = mode
+ self._is_dirty = False
+
+ try:
+ self._doc = self._storage.get_document(name)
+
+ tmp, ext = os.path.split(name)
+ if ext:
+ filename = "content." + ext
+ else:
+ filename = "content"
+ attachment = self._storage.db.get_attachment(self._doc, filename=filename)
+ self.file = StringIO(attachment)
+ except couchdb.client.ResourceNotFound:
+ if 'r' in self._mode:
+ raise ValueError("The file cannot be reopened.")
+ else:
+ self.file = StringIO()
+ self._is_dirty = True
+
+ @property
+ def size(self):
+ return self._doc['size']
+
+ def write(self, content):
+ if 'w' not in self._mode:
+ raise AttributeError("File was opened for read-only access.")
+ self.file = StringIO(content)
+ self._is_dirty = True
+
+ def close(self):
+ if self._is_dirty:
+ self._storage._put_file(self._name, self.file.getvalue())
+ self.file.close()
+
+
diff --git a/storages/backends/database.py b/storages/backends/database.py
new file mode 100644
index 0000000..3dd1259
--- /dev/null
+++ b/storages/backends/database.py
@@ -0,0 +1,132 @@
+# DatabaseStorage for django.
+# 2009 (c) GameKeeper Gambling Ltd, Ivanov E.
+import StringIO
+import urlparse
+
+from django.conf import settings
+from django.core.files import File
+from django.core.files.storage import Storage
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ import pyodbc
+except ImportError:
+ raise ImproperlyConfigured("Could not load pyodbc dependency.\
+ \nSee http://code.google.com/p/pyodbc/")
+
+REQUIRED_FIELDS = ('db_table', 'fname_column', 'blob_column', 'size_column', 'base_url')
+
+class DatabaseStorage(Storage):
+ """
+ Class DatabaseStorage provides storing files in the database.
+ """
+
+ def __init__(self, option=settings.DB_FILES):
+ """Constructor.
+
+ Constructs object using dictionary either specified in contucotr or
+in settings.DB_FILES.
+
+ @param option dictionary with 'db_table', 'fname_column',
+'blob_column', 'size_column', 'base_url' keys.
+
+ option['db_table']
+ Table to work with.
+ option['fname_column']
+ Column in the 'db_table' containing filenames (filenames can
+contain pathes). Values should be the same as where FileField keeps
+filenames.
+ It is used to map filename to blob_column. In sql it's simply
+used in where clause.
+ option['blob_column']
+ Blob column (for example 'image' type), created manually in the
+'db_table', used to store image.
+ option['size_column']
+ Column to store file size. Used for optimization of size()
+method (another way is to open file and get size)
+ option['base_url']
+ Url prefix used with filenames. Should be mapped to the view,
+that returns an image as result.
+ """
+
+ if not option or not all([field in option for field in REQUIRED_FIELDS]):
+ raise ValueError("You didn't specify required options")
+
+ self.db_table = option['db_table']
+ self.fname_column = option['fname_column']
+ self.blob_column = option['blob_column']
+ self.size_column = option['size_column']
+ self.base_url = option['base_url']
+
+ #get database settings
+ self.DATABASE_ODBC_DRIVER = settings.DATABASE_ODBC_DRIVER
+ self.DATABASE_NAME = settings.DATABASE_NAME
+ self.DATABASE_USER = settings.DATABASE_USER
+ self.DATABASE_PASSWORD = settings.DATABASE_PASSWORD
+ self.DATABASE_HOST = settings.DATABASE_HOST
+
+ self.connection = pyodbc.connect('DRIVER=%s;SERVER=%s;DATABASE=%s;UID=%s;PWD=%s'%(self.DATABASE_ODBC_DRIVER,self.DATABASE_HOST,self.DATABASE_NAME,
+ self.DATABASE_USER, self.DATABASE_PASSWORD) )
+ self.cursor = self.connection.cursor()
+
+ def _open(self, name, mode='rb'):
+ """Open a file from database.
+
+ @param name filename or relative path to file based on base_url. path should contain only "/", but not "\". Apache sends pathes with "/".
+ If there is no such file in the db, returs None
+ """
+
+ assert mode == 'rb', "You've tried to open binary file without specifying binary mode! You specified: %s"%mode
+
+ row = self.cursor.execute("SELECT %s from %s where %s = '%s'"%(self.blob_column,self.db_table,self.fname_column,name) ).fetchone()
+ if row is None:
+ return None
+ inMemFile = StringIO.StringIO(row[0])
+ inMemFile.name = name
+ inMemFile.mode = mode
+
+ retFile = File(inMemFile)
+ return retFile
+
+ def _save(self, name, content):
+ """Save 'content' as file named 'name'.
+
+ @note '\' in path will be converted to '/'.
+ """
+
+ name = name.replace('\\', '/')
+ binary = pyodbc.Binary(content.read())
+ size = len(binary)
+
+ #todo: check result and do something (exception?) if failed.
+ if self.exists(name):
+ self.cursor.execute("UPDATE %s SET %s = ?, %s = ? WHERE %s = '%s'"%(self.db_table,self.blob_column,self.size_column,self.fname_column,name),
+ (binary, size) )
+ else:
+ self.cursor.execute("INSERT INTO %s VALUES(?, ?, ?)"%(self.db_table), (name, binary, size) )
+ self.connection.commit()
+ return name
+
+ def exists(self, name):
+ row = self.cursor.execute("SELECT %s from %s where %s = '%s'"%(self.fname_column,self.db_table,self.fname_column,name)).fetchone()
+ return row is not None
+
+ def get_available_name(self, name):
+ return name
+
+ def delete(self, name):
+ if self.exists(name):
+ self.cursor.execute("DELETE FROM %s WHERE %s = '%s'"%(self.db_table,self.fname_column,name))
+ self.connection.commit()
+
+ def url(self, name):
+ if self.base_url is None:
+ raise ValueError("This file is not accessible via a URL.")
+ return urlparse.urljoin(self.base_url, name).replace('\\', '/')
+
+ def size(self, name):
+ row = self.cursor.execute("SELECT %s from %s where %s = '%s'"%(self.size_column,self.db_table,self.fname_column,name)).fetchone()
+ if row is None:
+ return 0
+ else:
+ return int(row[0])
diff --git a/storages/backends/ftp.py b/storages/backends/ftp.py
new file mode 100644
index 0000000..0012444
--- /dev/null
+++ b/storages/backends/ftp.py
@@ -0,0 +1,261 @@
+# FTP storage class for Django pluggable storage system.
+# Author: Rafal Jonca <jonca.rafal@gmail.com>
+# License: MIT
+# Comes from http://www.djangosnippets.org/snippets/1269/
+#
+# Usage:
+#
+# Add below to settings.py:
+# FTP_STORAGE_LOCATION = '[a]ftp://<user>:<pass>@<host>:<port>/[path]'
+#
+# In models.py you can write:
+# from FTPStorage import FTPStorage
+# fs = FTPStorage()
+# class FTPTest(models.Model):
+# file = models.FileField(upload_to='a/b/c/', storage=fs)
+
+import os
+import ftplib
+import urlparse
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+from django.conf import settings
+from django.core.files.base import File
+from django.core.files.storage import Storage
+from django.core.exceptions import ImproperlyConfigured
+
+
+class FTPStorageException(Exception):
+ pass
+
+
+class FTPStorage(Storage):
+ """FTP Storage class for Django pluggable storage system."""
+
+ def __init__(self, location=settings.FTP_STORAGE_LOCATION,
+ base_url=settings.MEDIA_URL):
+ self._config = self._decode_location(location)
+ self._base_url = base_url
+ self._connection = None
+
+ def _decode_location(self, location):
+ """Return splitted configuration data from location."""
+ splitted_url = urlparse.urlparse(location)
+ config = {}
+
+ if splitted_url.scheme not in ('ftp', 'aftp'):
+ raise ImproperlyConfigured(
+ 'FTPStorage works only with FTP protocol!'
+ )
+ if splitted_url.hostname == '':
+ raise ImproperlyConfigured('You must at least provide hostname!')
+
+ if splitted_url.scheme == 'aftp':
+ config['active'] = True
+ else:
+ config['active'] = False
+ config['path'] = splitted_url.path
+ config['host'] = splitted_url.hostname
+ config['user'] = splitted_url.username
+ config['passwd'] = splitted_url.password
+ config['port'] = int(splitted_url.port)
+
+ return config
+
+ def _start_connection(self):
+ # Check if connection is still alive and if not, drop it.
+ if self._connection is not None:
+ try:
+ self._connection.pwd()
+ except ftplib.all_errors:
+ self._connection = None
+
+ # Real reconnect
+ if self._connection is None:
+ ftp = ftplib.FTP()
+ try:
+ ftp.connect(self._config['host'], self._config['port'])
+ ftp.login(self._config['user'], self._config['passwd'])
+ if self._config['active']:
+ ftp.set_pasv(False)
+ if self._config['path'] != '':
+ ftp.cwd(self._config['path'])
+ self._connection = ftp
+ return
+ except ftplib.all_errors:
+ raise FTPStorageException(
+ 'Connection or login error using data %s'
+ % repr(self._config)
+ )
+
+ def disconnect(self):
+ self._connection.quit()
+ self._connection = None
+
+ def _mkremdirs(self, path):
+ pwd = self._connection.pwd()
+ path_splitted = path.split('/')
+ for path_part in path_splitted:
+ try:
+ self._connection.cwd(path_part)
+ except:
+ try:
+ self._connection.mkd(path_part)
+ self._connection.cwd(path_part)
+ except ftplib.all_errors:
+ raise FTPStorageException(
+ 'Cannot create directory chain %s' % path
+ )
+ self._connection.cwd(pwd)
+ return
+
+ def _put_file(self, name, content):
+ # Connection must be open!
+ try:
+ self._mkremdirs(os.path.dirname(name))
+ pwd = self._connection.pwd()
+ self._connection.cwd(os.path.dirname(name))
+ self._connection.storbinary('STOR ' + os.path.basename(name),
+ content.file,
+ content.DEFAULT_CHUNK_SIZE)
+ self._connection.cwd(pwd)
+ except ftplib.all_errors:
+ raise FTPStorageException('Error writing file %s' % name)
+
+ def _open(self, name, mode='rb'):
+ remote_file = FTPStorageFile(name, self, mode=mode)
+ return remote_file
+
+ def _read(self, name):
+ memory_file = StringIO()
+ try:
+ pwd = self._connection.pwd()
+ self._connection.cwd(os.path.dirname(name))
+ self._connection.retrbinary('RETR ' + os.path.basename(name),
+ memory_file.write)
+ self._connection.cwd(pwd)
+ return memory_file
+ except ftplib.all_errors:
+ raise FTPStorageException('Error reading file %s' % name)
+
+ def _save(self, name, content):
+ content.open()
+ self._start_connection()
+ self._put_file(name, content)
+ content.close()
+ return name
+
+ def _get_dir_details(self, path):
+ # Connection must be open!
+ try:
+ lines = []
+ self._connection.retrlines('LIST ' + path, lines.append)
+ dirs = {}
+ files = {}
+ for line in lines:
+ words = line.split()
+ if len(words) < 6:
+ continue
+ if words[-2] == '->':
+ continue
+ if words[0][0] == 'd':
+ dirs[words[-1]] = 0
+ elif words[0][0] == '-':
+ files[words[-1]] = int(words[-5])
+ return dirs, files
+ except ftplib.all_errors:
+ raise FTPStorageException('Error getting listing for %s' % path)
+
+ def listdir(self, path):
+ self._start_connection()
+ try:
+ dirs, files = self._get_dir_details(path)
+ return dirs.keys(), files.keys()
+ except FTPStorageException:
+ raise
+
+ def delete(self, name):
+ if not self.exists(name):
+ return
+ self._start_connection()
+ try:
+ self._connection.delete(name)
+ except ftplib.all_errors:
+ raise FTPStorageException('Error when removing %s' % name)
+
+ def exists(self, name):
+ self._start_connection()
+ try:
+ if os.path.basename(name) in self._connection.nlst(
+ os.path.dirname(name) + '/'
+ ):
+ return True
+ else:
+ return False
+ except ftplib.error_temp:
+ return False
+ except ftplib.error_perm:
+ # error_perm: 550 Can't find file
+ return False
+ except ftplib.all_errors:
+ raise FTPStorageException('Error when testing existence of %s'
+ % name)
+
+ def size(self, name):
+ self._start_connection()
+ try:
+ dirs, files = self._get_dir_details(os.path.dirname(name))
+ if os.path.basename(name) in files:
+ return files[os.path.basename(name)]
+ else:
+ return 0
+ except FTPStorageException:
+ return 0
+
+ def url(self, name):
+ if self._base_url is None:
+ raise ValueError("This file is not accessible via a URL.")
+ return urlparse.urljoin(self._base_url, name).replace('\\', '/')
+
+
+class FTPStorageFile(File):
+ def __init__(self, name, storage, mode):
+ self._name = name
+ self._storage = storage
+ self._mode = mode
+ self._is_dirty = False
+ self.file = StringIO()
+ self._is_read = False
+
+ @property
+ def size(self):
+ if not hasattr(self, '_size'):
+ self._size = self._storage.size(self._name)
+ return self._size
+
+ def read(self, num_bytes=None):
+ if not self._is_read:
+ self._storage._start_connection()
+ self.file = self._storage._read(self._name)
+ self._storage._end_connection()
+ self._is_read = True
+
+ return self.file.read(num_bytes)
+
+ def write(self, content):
+ if 'w' not in self._mode:
+ raise AttributeError("File was opened for read-only access.")
+ self.file = StringIO(content)
+ self._is_dirty = True
+ self._is_read = True
+
+ def close(self):
+ if self._is_dirty:
+ self._storage._start_connection()
+ self._storage._put_file(self._name, self)
+ self._storage.disconnect()
+ self.file.close()
diff --git a/storages/backends/gs.py b/storages/backends/gs.py
new file mode 100644
index 0000000..d9ef483
--- /dev/null
+++ b/storages/backends/gs.py
@@ -0,0 +1,99 @@
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO # noqa
+
+from django.core.exceptions import ImproperlyConfigured
+
+from storages.backends.s3boto import S3BotoStorage, S3BotoStorageFile
+from storages.utils import setting
+
+try:
+ from boto.gs.connection import GSConnection, SubdomainCallingFormat
+ from boto.exception import GSResponseError
+ from boto.gs.key import Key as GSKey
+except ImportError:
+ raise ImproperlyConfigured("Could not load Boto's Google Storage bindings.\n"
+ "See https://github.com/boto/boto")
+
+
+class GSBotoStorageFile(S3BotoStorageFile):
+
+ def write(self, content):
+ if 'w' not in self._mode:
+ raise AttributeError("File was not opened in write mode.")
+ self.file = StringIO(content)
+ self._is_dirty = True
+
+ def close(self):
+ if self._is_dirty:
+ provider = self.key.bucket.connection.provider
+ upload_headers = {provider.acl_header: self._storage.default_acl}
+ upload_headers.update(self._storage.headers)
+ self._storage._save_content(self.key, self.file, upload_headers)
+ self.key.close()
+
+
+class GSBotoStorage(S3BotoStorage):
+ connection_class = GSConnection
+ connection_response_error = GSResponseError
+ file_class = GSBotoStorageFile
+ key_class = GSKey
+
+ access_key_names = ['GS_ACCESS_KEY_ID']
+ secret_key_names = ['GS_SECRET_ACCESS_KEY']
+
+ access_key = setting('GS_ACCESS_KEY_ID')
+ secret_key = setting('GS_SECRET_ACCESS_KEY')
+ file_overwrite = setting('GS_FILE_OVERWRITE', True)
+ headers = setting('GS_HEADERS', {})
+ bucket_name = setting('GS_BUCKET_NAME', None)
+ auto_create_bucket = setting('GS_AUTO_CREATE_BUCKET', False)
+ default_acl = setting('GS_DEFAULT_ACL', 'public-read')
+ bucket_acl = setting('GS_BUCKET_ACL', default_acl)
+ querystring_auth = setting('GS_QUERYSTRING_AUTH', True)
+ querystring_expire = setting('GS_QUERYSTRING_EXPIRE', 3600)
+ durable_reduced_availability = setting('GS_DURABLE_REDUCED_AVAILABILITY', False)
+ location = setting('GS_LOCATION', '')
+ custom_domain = setting('GS_CUSTOM_DOMAIN')
+ calling_format = setting('GS_CALLING_FORMAT', SubdomainCallingFormat())
+ secure_urls = setting('GS_SECURE_URLS', True)
+ file_name_charset = setting('GS_FILE_NAME_CHARSET', 'utf-8')
+ is_gzipped = setting('GS_IS_GZIPPED', False)
+ preload_metadata = setting('GS_PRELOAD_METADATA', False)
+ gzip_content_types = setting('GS_GZIP_CONTENT_TYPES', (
+ 'text/css',
+ 'application/javascript',
+ 'application/x-javascript',
+ ))
+ url_protocol = setting('GS_URL_PROTOCOL', 'http:')
+
+ def _save_content(self, key, content, headers):
+ # only pass backwards incompatible arguments if they vary from the default
+ options = {}
+ if self.encryption:
+ options['encrypt_key'] = self.encryption
+ key.set_contents_from_file(content, headers=headers,
+ policy=self.default_acl,
+ rewind=True, **options)
+
+ def _get_or_create_bucket(self, name):
+ """
+ Retrieves a bucket if it exists, otherwise creates it.
+ """
+ if self.durable_reduced_availability:
+ storage_class = 'DURABLE_REDUCED_AVAILABILITY'
+ else:
+ storage_class = 'STANDARD'
+ try:
+ return self.connection.get_bucket(name,
+ validate=self.auto_create_bucket)
+ except self.connection_response_error:
+ if self.auto_create_bucket:
+ bucket = self.connection.create_bucket(name, storage_class=storage_class)
+ bucket.set_acl(self.bucket_acl)
+ return bucket
+ raise ImproperlyConfigured("Bucket %s does not exist. Buckets "
+ "can be automatically created by "
+ "setting GS_AUTO_CREATE_BUCKET to "
+ "``True``." % name)
diff --git a/storages/backends/hashpath.py b/storages/backends/hashpath.py
new file mode 100644
index 0000000..7603604
--- /dev/null
+++ b/storages/backends/hashpath.py
@@ -0,0 +1,40 @@
+import os, hashlib, errno
+
+from django.core.files.storage import FileSystemStorage
+from django.utils.encoding import force_unicode
+
+class HashPathStorage(FileSystemStorage):
+ """
+ Creates a hash from the uploaded file to build the path.
+ """
+
+ def save(self, name, content):
+ # Get the content name if name is not given
+ if name is None: name = content.name
+
+ # Get the SHA1 hash of the uploaded file
+ sha1 = hashlib.sha1()
+ for chunk in content.chunks():
+ sha1.update(chunk)
+ sha1sum = sha1.hexdigest()
+
+ # Build the new path and split it into directory and filename
+ name = os.path.join(os.path.split(name)[0], sha1sum[:1], sha1sum[1:2], sha1sum)
+ dir_name, file_name = os.path.split(name)
+
+ # Return the name if the file is already there
+ if self.exists(name):
+ return name
+
+ # Try to create the directory relative to location specified in __init__
+ try:
+ os.makedirs(os.path.join(self.location, dir_name))
+ except OSError as e:
+ if e.errno is not errno.EEXIST:
+ raise e
+
+ # Save the file
+ name = self._save(name, content)
+
+ # Store filenames with forward slashes, even on Windows
+ return force_unicode(name.replace('\\', '/'))
diff --git a/storages/backends/image.py b/storages/backends/image.py
new file mode 100644
index 0000000..ccad24d
--- /dev/null
+++ b/storages/backends/image.py
@@ -0,0 +1,55 @@
+
+import os
+
+from django.core.files.storage import FileSystemStorage
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ from PIL import ImageFile as PILImageFile
+except ImportError:
+ raise ImproperlyConfigured("Could not load PIL dependency.\
+ \nSee http://www.pythonware.com/products/pil/")
+
+
+class ImageStorage(FileSystemStorage):
+ """
+ A FileSystemStorage which normalizes extensions for images.
+
+ Comes from http://www.djangosnippets.org/snippets/965/
+ """
+
+ def find_extension(self, format):
+ """Normalizes PIL-returned format into a standard, lowercase extension."""
+ format = format.lower()
+
+ if format == 'jpeg':
+ format = 'jpg'
+
+ return format
+
+ def save(self, name, content):
+ dirname = os.path.dirname(name)
+ basename = os.path.basename(name)
+
+ # Use PIL to determine filetype
+
+ p = PILImageFile.Parser()
+ while 1:
+ data = content.read(1024)
+ if not data:
+ break
+ p.feed(data)
+ if p.image:
+ im = p.image
+ break
+
+ extension = self.find_extension(im.format)
+
+ # Does the basename already have an extension? If so, replace it.
+ # bare as in without extension
+ bare_basename, _ = os.path.splitext(basename)
+ basename = bare_basename + '.' + extension
+
+ name = os.path.join(dirname, basename)
+ return super(ImageStorage, self).save(name, content)
+
diff --git a/storages/backends/mogile.py b/storages/backends/mogile.py
new file mode 100644
index 0000000..e609ec7
--- /dev/null
+++ b/storages/backends/mogile.py
@@ -0,0 +1,118 @@
+from __future__ import print_function
+
+import urlparse
+import mimetypes
+from StringIO import StringIO
+
+from django.conf import settings
+from django.core.cache import cache
+from django.utils.text import force_unicode
+from django.core.files.storage import Storage
+from django.http import HttpResponse, HttpResponseNotFound
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ import mogilefs
+except ImportError:
+ raise ImproperlyConfigured("Could not load mogilefs dependency.\
+ \nSee http://mogilefs.pbworks.com/Client-Libraries")
+
+
+class MogileFSStorage(Storage):
+ """MogileFS filesystem storage"""
+ def __init__(self, base_url=settings.MEDIA_URL):
+
+ # the MOGILEFS_MEDIA_URL overrides MEDIA_URL
+ if hasattr(settings, 'MOGILEFS_MEDIA_URL'):
+ self.base_url = settings.MOGILEFS_MEDIA_URL
+ else:
+ self.base_url = base_url
+
+ for var in ('MOGILEFS_TRACKERS', 'MOGILEFS_DOMAIN',):
+ if not hasattr(settings, var):
+ raise ImproperlyConfigured("You must define %s to use the MogileFS backend." % var)
+
+ self.trackers = settings.MOGILEFS_TRACKERS
+ self.domain = settings.MOGILEFS_DOMAIN
+ self.client = mogilefs.Client(self.domain, self.trackers)
+
+ def get_mogile_paths(self, filename):
+ return self.client.get_paths(filename)
+
+ # The following methods define the Backend API
+
+ def filesize(self, filename):
+ raise NotImplemented
+ #return os.path.getsize(self._get_absolute_path(filename))
+
+ def path(self, filename):
+ paths = self.get_mogile_paths(filename)
+ if paths:
+ return self.get_mogile_paths(filename)[0]
+ else:
+ return None
+
+ def url(self, filename):
+ return urlparse.urljoin(self.base_url, filename).replace('\\', '/')
+
+ def open(self, filename, mode='rb'):
+ raise NotImplemented
+ #return open(self._get_absolute_path(filename), mode)
+
+ def exists(self, filename):
+ return filename in self.client
+
+ def save(self, filename, raw_contents):
+ filename = self.get_available_filename(filename)
+
+ if not hasattr(self, 'mogile_class'):
+ self.mogile_class = None
+
+ # Write the file to mogile
+ success = self.client.send_file(filename, StringIO(raw_contents), self.mogile_class)
+ if success:
+ print("Wrote file to key %s, %s@%s" % (filename, self.domain, self.trackers[0]))
+ else:
+ print("FAILURE writing file %s" % (filename))
+
+ return force_unicode(filename.replace('\\', '/'))
+
+ def delete(self, filename):
+
+ self.client.delete(filename)
+
+
+def serve_mogilefs_file(request, key=None):
+ """
+ Called when a user requests an image.
+ Either reproxy the path to perlbal, or serve the image outright
+ """
+ # not the best way to do this, since we create a client each time
+ mimetype = mimetypes.guess_type(key)[0] or "application/x-octet-stream"
+ client = mogilefs.Client(settings.MOGILEFS_DOMAIN, settings.MOGILEFS_TRACKERS)
+ if hasattr(settings, "SERVE_WITH_PERLBAL") and settings.SERVE_WITH_PERLBAL:
+ # we're reproxying with perlbal
+
+ # check the path cache
+
+ path = cache.get(key)
+
+ if not path:
+ path = client.get_paths(key)
+ cache.set(key, path, 60)
+
+ if path:
+ response = HttpResponse(content_type=mimetype)
+ response['X-REPROXY-URL'] = path[0]
+ else:
+ response = HttpResponseNotFound()
+
+ else:
+ # we don't have perlbal, let's just serve the image via django
+ file_data = client[key]
+ if file_data:
+ response = HttpResponse(file_data, mimetype=mimetype)
+ else:
+ response = HttpResponseNotFound()
+
+ return response
diff --git a/storages/backends/mongodb.py b/storages/backends/mongodb.py
new file mode 100644
index 0000000..b963729
--- /dev/null
+++ b/storages/backends/mongodb.py
@@ -0,0 +1,104 @@
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.files.base import File
+from django.core.files.storage import Storage
+from django.db import connections
+from django.utils.encoding import force_unicode
+
+try:
+ from gridfs import GridFS, NoFile
+except ImportError:
+ raise ImproperlyConfigured("Could not load gridfs dependency.\
+ \nSee http://www.mongodb.org/display/DOCS/GridFS")
+
+try:
+ from pymongo import Connection
+except ImportError:
+ raise ImproperlyConfigured("Could not load pymongo dependency.\
+ \nSee http://github.com/mongodb/mongo-python-driver")
+
+class GridFSStorage(Storage):
+ @property
+ def fs(self):
+ db = settings.GRIDFS_DATABASE
+ # This should support both the django_mongodb_engine and the GSoC 2010
+ # MongoDB backend
+ from django_mongodb_engine import __version__
+ if __version__[0] == 0 and __version__[1] <= 3:
+ try:
+ connection = connections[db].db_connection
+ except:
+ connection = connections[db].connection
+ return GridFS(connection)
+ else:
+ return GridFS(connections[db].database)
+
+ def _open(self, name, mode='rb'):
+ return GridFSFile(name, self, mode=mode)
+
+ def _save(self, name, content):
+ name = force_unicode(name).replace('\\', '/')
+ content.open()
+ kwargs = {'filename': name}
+ if hasattr(content.file, 'content_type'):
+ kwargs['content_type'] = content.file.content_type
+ file = self.fs.new_file(**kwargs)
+ if hasattr(content, 'chunks'):
+ for chunk in content.chunks():
+ file.write(chunk)
+ else:
+ file.write(content)
+ file.close()
+ content.close()
+ return name
+
+ def get_valid_name(self, name):
+ return force_unicode(name).strip().replace('\\', '/')
+
+ def delete(self, name):
+ f = self._open(name, 'r')
+ return self.fs.delete(f.file._id)
+
+ def exists(self, name):
+ try:
+ self.fs.get_last_version(name)
+ return True
+ except NoFile:
+ return False
+
+ def listdir(self, path):
+ return ((), self.fs.list())
+
+ def size(self, name):
+ try:
+ return self.fs.get_last_version(name).length
+ except NoFile:
+ raise ValueError('File with name "%s" does not exist' % name)
+
+ def url(self, name):
+ raise NotImplementedError()
+
+class GridFSFile(File):
+ def __init__(self, name, storage, mode):
+ self.name = name
+ self._storage = storage
+ self._mode = mode
+
+ try:
+ self.file = storage.fs.get_last_version(name)
+ except NoFile:
+ raise ValueError("The file doesn't exist.")
+
+ @property
+ def size(self):
+ return self.file.length
+
+ def read(self, num_bytes=None):
+ return self.file.read(num_bytes)
+
+ def write(self, content):
+ raise NotImplementedError()
+
+ def close(self):
+ self.file.close()
+
diff --git a/storages/backends/mosso.py b/storages/backends/mosso.py
new file mode 100644
index 0000000..5a4812d
--- /dev/null
+++ b/storages/backends/mosso.py
@@ -0,0 +1,347 @@
+"""
+Custom storage for django with Mosso Cloud Files backend.
+Created by Rich Leland <rich@richleland.com>.
+"""
+import os
+import warnings
+warnings.simplefilter('always', PendingDeprecationWarning)
+warnings.warn("The mosso module will be deprecated in version 1.2 of "
+ "django-storages. The CloudFiles code has been moved into"
+ "django-cumulus at http://github.com/richleland/django-cumulus.",
+ PendingDeprecationWarning)
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.files import File
+from django.core.files.storage import Storage
+from django.utils.text import get_valid_filename
+
+try:
+ from cStringIO import StringIO
+except:
+ from StringIO import StringIO
+
+try:
+ import cloudfiles
+ from cloudfiles.errors import NoSuchObject
+except ImportError:
+ raise ImproperlyConfigured("Could not load cloudfiles dependency. See "
+ "http://www.mosso.com/cloudfiles.jsp.")
+
+# TODO: implement TTL into cloudfiles methods
+TTL = getattr(settings, 'CLOUDFILES_TTL', 600)
+CONNECTION_KWARGS = getattr(settings, 'CLOUDFILES_CONNECTION_KWARGS', {})
+SSL = getattr(settings, 'CLOUDFILES_SSL', False)
+
+
+def cloudfiles_upload_to(self, filename):
+ """
+ Simple, custom upload_to because Cloud Files doesn't support
+ nested containers (directories).
+
+ Actually found this out from @minter:
+ @richleland The Cloud Files APIs do support pseudo-subdirectories, by
+ creating zero-byte files with type application/directory.
+
+ May implement in a future version.
+ """
+ return get_valid_filename(filename)
+
+
+class CloudFilesStorage(Storage):
+ """
+ Custom storage for Mosso Cloud Files.
+ """
+ default_quick_listdir = True
+
+ def __init__(self,
+ username=settings.CLOUDFILES_USERNAME,
+ api_key=settings.CLOUDFILES_API_KEY,
+ container=settings.CLOUDFILES_CONTAINER,
+ connection_kwargs=CONNECTION_KWARGS):
+ """
+ Initialize the settings for the connection and container.
+ """
+ self.username = username
+ self.api_key = api_key
+ self.container_name = container
+ self.connection_kwargs = connection_kwargs
+
+ def __getstate__(self):
+ """
+ Return a picklable representation of the storage.
+ """
+ return dict(username=self.username,
+ api_key=self.api_key,
+ container_name=self.container_name,
+ connection_kwargs=self.connection_kwargs)
+
+ def _get_connection(self):
+ if not hasattr(self, '_connection'):
+ self._connection = cloudfiles.get_connection(self.username,
+ self.api_key, **self.connection_kwargs)
+ return self._connection
+
+ def _set_connection(self, value):
+ self._connection = value
+
+ connection = property(_get_connection, _set_connection)
+
+ def _get_container(self):
+ if not hasattr(self, '_container'):
+ self.container = self.connection.get_container(
+ self.container_name)
+ return self._container
+
+ def _set_container(self, container):
+ """
+ Set the container, making it publicly available (on Limelight CDN) if
+ it is not already.
+ """
+ if not container.is_public():
+ container.make_public()
+ if hasattr(self, '_container_public_uri'):
+ delattr(self, '_container_public_uri')
+ self._container = container
+
+ container = property(_get_container, _set_container)
+
+ def _get_container_url(self):
+ if not hasattr(self, '_container_public_uri'):
+ if SSL:
+ self._container_public_uri = self.container.public_ssl_uri()
+ else:
+ self._container_public_uri = self.container.public_uri()
+ return self._container_public_uri
+
+ container_url = property(_get_container_url)
+
+ def _get_cloud_obj(self, name):
+ """
+ Helper function to get retrieve the requested Cloud Files Object.
+ """
+ return self.container.get_object(name)
+
+ def _open(self, name, mode='rb'):
+ """
+ Return the CloudFilesStorageFile.
+ """
+ return CloudFilesStorageFile(storage=self, name=name)
+
+ def _save(self, name, content):
+ """
+ Use the Cloud Files service to write ``content`` to a remote file
+ (called ``name``).
+ """
+ (path, last) = os.path.split(name)
+ if path:
+ try:
+ self.container.get_object(path)
+ except NoSuchObject:
+ self._save(path, CloudStorageDirectory(path))
+
+ cloud_obj = self.container.create_object(name)
+ cloud_obj.size = content.size
+
+ content.open()
+ # If the content type is available, pass it in directly rather than
+ # getting the cloud object to try to guess.
+ if hasattr(content.file, 'content_type'):
+ cloud_obj.content_type = content.file.content_type
+ cloud_obj.send(content)
+ content.close()
+ return name
+
+ def delete(self, name):
+ """
+ Deletes the specified file from the storage system.
+ """
+ # If the file exists, delete it.
+ if self.exists(name):
+ self.container.delete_object(name)
+
+ def exists(self, name):
+ """
+ Returns True if a file referenced by the given name already exists in
+ the storage system, or False if the name is available for a new file.
+ """
+ try:
+ self._get_cloud_obj(name)
+ return True
+ except NoSuchObject:
+ return False
+
+ def listdir(self, path):
+ """
+ Lists the contents of the specified path, returning a 2-tuple; the
+ first being an empty list of directories (not available for quick-
+ listing), the second being a list of filenames.
+
+ If the list of directories is required, use the full_listdir method.
+ """
+ files = []
+ if path and not path.endswith('/'):
+ path = '%s/' % path
+ path_len = len(path)
+ for name in self.container.list_objects(path=path):
+ files.append(name[path_len:])
+ return ([], files)
+
+ def full_listdir(self, path):
+ """
+ Lists the contents of the specified path, returning a 2-tuple of lists;
+ the first item being directories, the second item being files.
+
+ On large containers, this may be a slow operation for root containers
+ because every single object must be returned (cloudfiles does not
+ provide an explicit way of listing directories).
+ """
+ dirs = set()
+ files = []
+ if path and not path.endswith('/'):
+ path = '%s/' % path
+ path_len = len(path)
+ for name in self.container.list_objects(prefix=path):
+ name = name[path_len:]
+ slash = name[1:-1].find('/') + 1
+ if slash:
+ dirs.add(name[:slash])
+ elif name:
+ files.append(name)
+ dirs = list(dirs)
+ dirs.sort()
+ return (dirs, files)
+
+ def size(self, name):
+ """
+ Returns the total size, in bytes, of the file specified by name.
+ """
+ return self._get_cloud_obj(name).size
+
+ def url(self, name):
+ """
+ Returns an absolute URL where the file's contents can be accessed
+ directly by a web browser.
+ """
+ return '%s/%s' % (self.container_url, name)
+
+
+class CloudStorageDirectory(File):
+ """
+ A File-like object that creates a directory at cloudfiles
+ """
+
+ def __init__(self, name):
+ super(CloudStorageDirectory, self).__init__(StringIO(), name=name)
+ self.file.content_type = 'application/directory'
+ self.size = 0
+
+ def __str__(self):
+ return 'directory'
+
+ def __nonzero__(self):
+ return True
+
+ def open(self, mode=None):
+ self.seek(0)
+
+ def close(self):
+ pass
+
+
+class CloudFilesStorageFile(File):
+ closed = False
+
+ def __init__(self, storage, name, *args, **kwargs):
+ self._storage = storage
+ super(CloudFilesStorageFile, self).__init__(file=None, name=name,
+ *args, **kwargs)
+ self._pos = 0
+
+
+ def _get_size(self):
+ if not hasattr(self, '_size'):
+ self._size = self._storage.size(self.name)
+ return self._size
+
+ def _set_size(self, size):
+ self._size = size
+
+ size = property(_get_size, _set_size)
+
+ def _get_file(self):
+ if not hasattr(self, '_file'):
+ self._file = self._storage._get_cloud_obj(self.name)
+ return self._file
+
+ def _set_file(self, value):
+ if value is None:
+ if hasattr(self, '_file'):
+ del self._file
+ else:
+ self._file = value
+
+ file = property(_get_file, _set_file)
+
+ def read(self, num_bytes=None):
+ if self._pos == self._get_size():
+ return None
+ if self._pos + num_bytes > self._get_size():
+ num_bytes = self._get_size() - self._pos
+ data = self.file.read(size=num_bytes or -1, offset=self._pos)
+ self._pos += len(data)
+ return data
+
+ def open(self, *args, **kwargs):
+ """
+ Open the cloud file object.
+ """
+ self.file
+ self._pos = 0
+
+ def close(self, *args, **kwargs):
+ self._pos = 0
+
+ @property
+ def closed(self):
+ return not hasattr(self, '_file')
+
+ def seek(self, pos):
+ self._pos = pos
+
+
+class ThreadSafeCloudFilesStorage(CloudFilesStorage):
+ """
+ Extends CloudFilesStorage to make it thread safer.
+
+ As long as you don't pass container or cloud objects
+ between threads, you'll be thread safe.
+
+ Uses one cloudfiles connection per thread.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(ThreadSafeCloudFilesStorage, self).__init__(*args, **kwargs)
+
+ import threading
+ self.local_cache = threading.local()
+
+ def _get_connection(self):
+ if not hasattr(self.local_cache, 'connection'):
+ connection = cloudfiles.get_connection(self.username,
+ self.api_key, **self.connection_kwargs)
+ self.local_cache.connection = connection
+
+ return self.local_cache.connection
+
+ connection = property(_get_connection, CloudFilesStorage._set_connection)
+
+ def _get_container(self):
+ if not hasattr(self.local_cache, 'container'):
+ container = self.connection.get_container(self.container_name)
+ self.local_cache.container = container
+
+ return self.local_cache.container
+
+ container = property(_get_container, CloudFilesStorage._set_container)
+
diff --git a/storages/backends/overwrite.py b/storages/backends/overwrite.py
new file mode 100644
index 0000000..fba464a
--- /dev/null
+++ b/storages/backends/overwrite.py
@@ -0,0 +1,19 @@
+from django.core.files.storage import FileSystemStorage
+
+
+class OverwriteStorage(FileSystemStorage):
+ """
+ Comes from http://www.djangosnippets.org/snippets/976/
+ (even if it already exists in S3Storage for ages)
+
+ See also Django #4339, which might add this functionality to core.
+ """
+
+ def get_available_name(self, name):
+ """
+ Returns a filename that's free on the target storage system, and
+ available for new content to be written to.
+ """
+ if self.exists(name):
+ self.delete(name)
+ return name
diff --git a/storages/backends/s3.py b/storages/backends/s3.py
new file mode 100644
index 0000000..a700596
--- /dev/null
+++ b/storages/backends/s3.py
@@ -0,0 +1,288 @@
+import os
+import mimetypes
+import warnings
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+from django.conf import settings
+from django.core.files.base import File
+from django.core.files.storage import Storage
+from django.core.exceptions import ImproperlyConfigured
+
+try:
+ from S3 import AWSAuthConnection, QueryStringAuthGenerator, CallingFormat
+except ImportError:
+ raise ImproperlyConfigured("Could not load amazon's S3 bindings.\nSee "
+ "http://developer.amazonwebservices.com/connect/entry.jspa?externalID=134")
+
+ACCESS_KEY_NAME = getattr(settings, 'AWS_S3_ACCESS_KEY_ID', getattr(settings, 'AWS_ACCESS_KEY_ID', None))
+SECRET_KEY_NAME = getattr(settings, 'AWS_S3_SECRET_ACCESS_KEY', getattr(settings, 'AWS_SECRET_ACCESS_KEY', None))
+HEADERS = getattr(settings, 'AWS_HEADERS', {})
+DEFAULT_ACL = getattr(settings, 'AWS_DEFAULT_ACL', 'public-read')
+QUERYSTRING_ACTIVE = getattr(settings, 'AWS_QUERYSTRING_ACTIVE', False)
+QUERYSTRING_EXPIRE = getattr(settings, 'AWS_QUERYSTRING_EXPIRE', 60)
+SECURE_URLS = getattr(settings, 'AWS_S3_SECURE_URLS', False)
+BUCKET_PREFIX = getattr(settings, 'AWS_BUCKET_PREFIX', '')
+CALLING_FORMAT = getattr(settings, 'AWS_CALLING_FORMAT', CallingFormat.PATH)
+PRELOAD_METADATA = getattr(settings, 'AWS_PRELOAD_METADATA', False)
+
+IS_GZIPPED = getattr(settings, 'AWS_IS_GZIPPED', False)
+GZIP_CONTENT_TYPES = getattr(settings, 'GZIP_CONTENT_TYPES', (
+ 'text/css',
+ 'application/javascript',
+ 'application/x-javascript'
+))
+
+if IS_GZIPPED:
+ from gzip import GzipFile
+
+class S3Storage(Storage):
+ """Amazon Simple Storage Service"""
+
+ def __init__(self, bucket=settings.AWS_STORAGE_BUCKET_NAME,
+ access_key=None, secret_key=None, acl=DEFAULT_ACL,
+ calling_format=CALLING_FORMAT, encrypt=False,
+ gzip=IS_GZIPPED, gzip_content_types=GZIP_CONTENT_TYPES,
+ preload_metadata=PRELOAD_METADATA):
+ warnings.warn(
+ "The s3 backend is deprecated and will be removed in version 1.2. "
+ "Use the s3boto backend instead.",
+ PendingDeprecationWarning
+ )
+ self.bucket = bucket
+ self.acl = acl
+ self.encrypt = encrypt
+ self.gzip = gzip
+ self.gzip_content_types = gzip_content_types
+ self.preload_metadata = preload_metadata
+
+ if encrypt:
+ try:
+ import ezPyCrypto
+ except ImportError:
+ raise ImproperlyConfigured("Could not load ezPyCrypto.\nSee "
+ "http://www.freenet.org.nz/ezPyCrypto/ to install it.")
+ self.crypto_key = ezPyCrypto.key
+
+ if not access_key and not secret_key:
+ access_key, secret_key = self._get_access_keys()
+
+ self.connection = AWSAuthConnection(access_key, secret_key,
+ calling_format=calling_format)
+ self.generator = QueryStringAuthGenerator(access_key, secret_key,
+ calling_format=calling_format,
+ is_secure=SECURE_URLS)
+ self.generator.set_expires_in(QUERYSTRING_EXPIRE)
+
+ self.headers = HEADERS
+ self._entries = {}
+
+ def _get_access_keys(self):
+ access_key = ACCESS_KEY_NAME
+ secret_key = SECRET_KEY_NAME
+ if (access_key or secret_key) and (not access_key or not secret_key):
+ access_key = os.environ.get(ACCESS_KEY_NAME)
+ secret_key = os.environ.get(SECRET_KEY_NAME)
+
+ if access_key and secret_key:
+ # Both were provided, so use them
+ return access_key, secret_key
+
+ return None, None
+
+ @property
+ def entries(self):
+ if self.preload_metadata and not self._entries:
+ self._entries = dict((entry.key, entry)
+ for entry in self.connection.list_bucket(self.bucket).entries)
+ return self._entries
+
+ def _get_connection(self):
+ return AWSAuthConnection(*self._get_access_keys())
+
+ def _clean_name(self, name):
+ # Useful for windows' paths
+ return os.path.join(BUCKET_PREFIX, os.path.normpath(name).replace('\\', '/'))
+
+ def _compress_string(self, s):
+ """Gzip a given string."""
+ zbuf = StringIO()
+ zfile = GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
+ zfile.write(s)
+ zfile.close()
+ return zbuf.getvalue()
+
+ def _put_file(self, name, content):
+ if self.encrypt:
+
+ # Create a key object
+ key = self.crypto_key()
+
+ # Read in a public key
+ fd = open(settings.CRYPTO_KEYS_PUBLIC, "rb")
+ public_key = fd.read()
+ fd.close()
+
+ # import this public key
+ key.importKey(public_key)
+
+ # Now encrypt some text against this public key
+ content = key.encString(content)
+
+ content_type = mimetypes.guess_type(name)[0] or "application/x-octet-stream"
+
+ if self.gzip and content_type in self.gzip_content_types:
+ content = self._compress_string(content)
+ self.headers.update({'Content-Encoding': 'gzip'})
+
+ self.headers.update({
+ 'x-amz-acl': self.acl,
+ 'Content-Type': content_type,
+ 'Content-Length' : str(len(content)),
+ })
+ response = self.connection.put(self.bucket, name, content, self.headers)
+ if response.http_response.status not in (200, 206):
+ raise IOError("S3StorageError: %s" % response.message)
+
+ def _open(self, name, mode='rb'):
+ name = self._clean_name(name)
+ remote_file = S3StorageFile(name, self, mode=mode)
+ return remote_file
+
+ def _read(self, name, start_range=None, end_range=None):
+ name = self._clean_name(name)
+ if start_range is None:
+ headers = {}
+ else:
+ headers = {'Range': 'bytes=%s-%s' % (start_range, end_range)}
+ response = self.connection.get(self.bucket, name, headers)
+ if response.http_response.status not in (200, 206):
+ raise IOError("S3StorageError: %s" % response.message)
+ headers = response.http_response.msg
+
+ if self.encrypt:
+ # Read in a private key
+ fd = open(settings.CRYPTO_KEYS_PRIVATE, "rb")
+ private_key = fd.read()
+ fd.close()
+
+ # Create a key object, and auto-import private key
+ key = self.crypto_key(private_key)
+
+ # Decrypt this file
+ response.object.data = key.decString(response.object.data)
+
+ return response.object.data, headers.get('etag', None), headers.get('content-range', None)
+
+ def _save(self, name, content):
+ name = self._clean_name(name)
+ content.open()
+ if hasattr(content, 'chunks'):
+ content_str = ''.join(chunk for chunk in content.chunks())
+ else:
+ content_str = content.read()
+ self._put_file(name, content_str)
+ return name
+
+ def delete(self, name):
+ name = self._clean_name(name)
+ response = self.connection.delete(self.bucket, name)
+ if response.http_response.status != 204:
+ raise IOError("S3StorageError: %s" % response.message)
+
+ def exists(self, name):
+ name = self._clean_name(name)
+ if self.entries:
+ return name in self.entries
+ response = self.connection._make_request('HEAD', self.bucket, name)
+ return response.status == 200
+
+ def size(self, name):
+ name = self._clean_name(name)
+ if self.entries:
+ entry = self.entries.get(name)
+ if entry:
+ return entry.size
+ return 0
+ response = self.connection._make_request('HEAD', self.bucket, name)
+ content_length = response.getheader('Content-Length')
+ return content_length and int(content_length) or 0
+
+ def url(self, name):
+ name = self._clean_name(name)
+ if QUERYSTRING_ACTIVE:
+ return self.generator.generate_url('GET', self.bucket, name)
+ else:
+ return self.generator.make_bare_url(self.bucket, name)
+
+ def modified_time(self, name):
+ try:
+ from dateutil import parser, tz
+ except ImportError:
+ raise NotImplementedError()
+ name = self._clean_name(name)
+ if self.entries:
+ last_modified = self.entries.get(name).last_modified
+ else:
+ response = self.connection._make_request('HEAD', self.bucket, name)
+ last_modified = response.getheader('Last-Modified')
+ # convert to string to date
+ last_modified_date = parser.parse(last_modified)
+ # if the date has no timzone, assume UTC
+ if last_modified_date.tzinfo == None:
+ last_modified_date = last_modified_date.replace(tzinfo=tz.tzutc())
+ # convert date to local time w/o timezone
+ return last_modified_date.astimezone(tz.tzlocal()).replace(tzinfo=None)
+
+ ## UNCOMMENT BELOW IF NECESSARY
+ #def get_available_name(self, name):
+ # """ Overwrite existing file with the same name. """
+ # name = self._clean_name(name)
+ # return name
+
+
+class PreloadingS3Storage(S3Storage):
+ pass
+
+class S3StorageFile(File):
+ def __init__(self, name, storage, mode):
+ self._name = name
+ self._storage = storage
+ self._mode = mode
+ self._is_dirty = False
+ self.file = StringIO()
+ self.start_range = 0
+
+ @property
+ def size(self):
+ if not hasattr(self, '_size'):
+ self._size = self._storage.size(self._name)
+ return self._size
+
+ def read(self, num_bytes=None):
+ if num_bytes is None:
+ args = []
+ self.start_range = 0
+ else:
+ args = [self.start_range, self.start_range+num_bytes-1]
+ data, etags, content_range = self._storage._read(self._name, *args)
+ if content_range is not None:
+ current_range, size = content_range.split(' ', 1)[1].split('/', 1)
+ start_range, end_range = current_range.split('-', 1)
+ self._size, self.start_range = int(size), int(end_range)+1
+ self.file = StringIO(data)
+ return self.file.getvalue()
+
+ def write(self, content):
+ if 'w' not in self._mode:
+ raise AttributeError("File was opened for read-only access.")
+ self.file = StringIO(content)
+ self._is_dirty = True
+
+ def close(self):
+ if self._is_dirty:
+ self._storage._put_file(self._name, self.file.getvalue())
+ self.file.close()
diff --git a/storages/backends/s3boto.py b/storages/backends/s3boto.py
new file mode 100644
index 0000000..354cd0c
--- /dev/null
+++ b/storages/backends/s3boto.py
@@ -0,0 +1,501 @@
+import os
+import posixpath
+import mimetypes
+from gzip import GzipFile
+import datetime
+from tempfile import SpooledTemporaryFile
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO # noqa
+
+from django.core.files.base import File
+from django.core.files.storage import Storage
+from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
+from django.utils.encoding import force_unicode, smart_str, filepath_to_uri
+
+try:
+ from boto import __version__ as boto_version
+ from boto.s3.connection import S3Connection, SubdomainCallingFormat
+ from boto.exception import S3ResponseError
+ from boto.s3.key import Key as S3Key
+ from boto.utils import parse_ts
+except ImportError:
+ raise ImproperlyConfigured("Could not load Boto's S3 bindings.\n"
+ "See https://github.com/boto/boto")
+
+from storages.utils import setting
+
+boto_version_info = tuple([int(i) for i in boto_version.split('-')[0].split('.')])
+
+if boto_version_info[:2] < (2, 4):
+ raise ImproperlyConfigured("The installed Boto library must be 2.4 or "
+ "higher.\nSee https://github.com/boto/boto")
+
+
+def parse_ts_extended(ts):
+ RFC1123 = '%a, %d %b %Y %H:%M:%S %Z'
+ rv = None
+ try:
+ rv = parse_ts(ts)
+ except ValueError:
+ rv = datetime.datetime.strptime(ts, RFC1123)
+ return rv
+
+
+def safe_join(base, *paths):
+ """
+ A version of django.utils._os.safe_join for S3 paths.
+
+ Joins one or more path components to the base path component
+ intelligently. Returns a normalized version of the final path.
+
+ The final path must be located inside of the base path component
+ (otherwise a ValueError is raised).
+
+ Paths outside the base path indicate a possible security
+ sensitive operation.
+ """
+ from urlparse import urljoin
+ base_path = force_unicode(base)
+ base_path = base_path.rstrip('/')
+ paths = [force_unicode(p) for p in paths]
+
+ final_path = base_path
+ for path in paths:
+ final_path = urljoin(final_path.rstrip('/') + "/", path)
+
+ # Ensure final_path starts with base_path and that the next character after
+ # the final path is '/' (or nothing, in which case final_path must be
+ # equal to base_path).
+ base_path_len = len(base_path)
+ if (not final_path.startswith(base_path) or
+ final_path[base_path_len:base_path_len + 1] not in ('', '/')):
+ raise ValueError('the joined path is located outside of the base path'
+ ' component')
+
+ return final_path.lstrip('/')
+
+
+class S3BotoStorageFile(File):
+ """
+ The default file object used by the S3BotoStorage backend.
+
+ This file implements file streaming using boto's multipart
+ uploading functionality. The file can be opened in read or
+ write mode.
+
+ This class extends Django's File class. However, the contained
+ data is only the data contained in the current buffer. So you
+ should not access the contained file object directly. You should
+ access the data via this class.
+
+ Warning: This file *must* be closed using the close() method in
+ order to properly write the file to S3. Be sure to close the file
+ in your application.
+ """
+ # TODO: Read/Write (rw) mode may be a bit undefined at the moment. Needs testing.
+ # TODO: When Django drops support for Python 2.5, rewrite to use the
+ # BufferedIO streams in the Python 2.6 io module.
+ buffer_size = setting('AWS_S3_FILE_BUFFER_SIZE', 5242880)
+
+ def __init__(self, name, mode, storage, buffer_size=None):
+ self._storage = storage
+ self.name = name[len(self._storage.location):].lstrip('/')
+ self._mode = mode
+ self.key = storage.bucket.get_key(self._storage._encode_name(name))
+ if not self.key and 'w' in mode:
+ self.key = storage.bucket.new_key(storage._encode_name(name))
+ self._is_dirty = False
+ self._file = None
+ self._multipart = None
+ # 5 MB is the minimum part size (if there is more than one part).
+ # Amazon allows up to 10,000 parts. The default supports uploads
+ # up to roughly 50 GB. Increase the part size to accommodate
+ # for files larger than this.
+ if buffer_size is not None:
+ self.buffer_size = buffer_size
+ self._write_counter = 0
+
+ @property
+ def size(self):
+ return self.key.size
+
+ def _get_file(self):
+ if self._file is None:
+ self._file = SpooledTemporaryFile(
+ max_size=self._storage.max_memory_size,
+ suffix=".S3BotoStorageFile",
+ dir=setting("FILE_UPLOAD_TEMP_DIR", None)
+ )
+ if 'r' in self._mode:
+ self._is_dirty = False
+ self.key.get_contents_to_file(self._file)
+ self._file.seek(0)
+ if self._storage.gzip and self.key.content_encoding == 'gzip':
+ self._file = GzipFile(mode=self._mode, fileobj=self._file)
+ return self._file
+
+ def _set_file(self, value):
+ self._file = value
+
+ file = property(_get_file, _set_file)
+
+ def read(self, *args, **kwargs):
+ if 'r' not in self._mode:
+ raise AttributeError("File was not opened in read mode.")
+ return super(S3BotoStorageFile, self).read(*args, **kwargs)
+
+ def write(self, *args, **kwargs):
+ if 'w' not in self._mode:
+ raise AttributeError("File was not opened in write mode.")
+ self._is_dirty = True
+ if self._multipart is None:
+ provider = self.key.bucket.connection.provider
+ upload_headers = {
+ provider.acl_header: self._storage.default_acl
+ }
+ upload_headers.update({'Content-Type': mimetypes.guess_type(self.key.name)[0] or self._storage.key_class.DefaultContentType})
+ upload_headers.update(self._storage.headers)
+ self._multipart = self._storage.bucket.initiate_multipart_upload(
+ self.key.name,
+ headers=upload_headers,
+ reduced_redundancy=self._storage.reduced_redundancy
+ )
+ if self.buffer_size <= self._buffer_file_size:
+ self._flush_write_buffer()
+ return super(S3BotoStorageFile, self).write(*args, **kwargs)
+
+ @property
+ def _buffer_file_size(self):
+ pos = self.file.tell()
+ self.file.seek(0, os.SEEK_END)
+ length = self.file.tell()
+ self.file.seek(pos)
+ return length
+
+ def _flush_write_buffer(self):
+ """
+ Flushes the write buffer.
+ """
+ if self._buffer_file_size:
+ self._write_counter += 1
+ self.file.seek(0)
+ headers = self._storage.headers.copy()
+ self._multipart.upload_part_from_file(
+ self.file, self._write_counter, headers=headers)
+ self.file.close()
+ self._file = None
+
+ def close(self):
+ if self._is_dirty:
+ self._flush_write_buffer()
+ self._multipart.complete_upload()
+ else:
+ if not self._multipart is None:
+ self._multipart.cancel_upload()
+ self.key.close()
+
+
+class S3BotoStorage(Storage):
+ """
+ Amazon Simple Storage Service using Boto
+
+ This storage backend supports opening files in read or write
+ mode and supports streaming(buffering) data in chunks to S3
+ when writing.
+ """
+ connection_class = S3Connection
+ connection_response_error = S3ResponseError
+ file_class = S3BotoStorageFile
+ key_class = S3Key
+
+ # used for looking up the access and secret key from env vars
+ access_key_names = ['AWS_S3_ACCESS_KEY_ID', 'AWS_ACCESS_KEY_ID']
+ secret_key_names = ['AWS_S3_SECRET_ACCESS_KEY', 'AWS_SECRET_ACCESS_KEY']
+
+ access_key = setting('AWS_S3_ACCESS_KEY_ID', setting('AWS_ACCESS_KEY_ID'))
+ secret_key = setting('AWS_S3_SECRET_ACCESS_KEY', setting('AWS_SECRET_ACCESS_KEY'))
+ file_overwrite = setting('AWS_S3_FILE_OVERWRITE', True)
+ headers = setting('AWS_HEADERS', {})
+ bucket_name = setting('AWS_STORAGE_BUCKET_NAME')
+ auto_create_bucket = setting('AWS_AUTO_CREATE_BUCKET', False)
+ default_acl = setting('AWS_DEFAULT_ACL', 'public-read')
+ bucket_acl = setting('AWS_BUCKET_ACL', default_acl)
+ querystring_auth = setting('AWS_QUERYSTRING_AUTH', True)
+ querystring_expire = setting('AWS_QUERYSTRING_EXPIRE', 3600)
+ reduced_redundancy = setting('AWS_REDUCED_REDUNDANCY', False)
+ location = setting('AWS_LOCATION', '')
+ encryption = setting('AWS_S3_ENCRYPTION', False)
+ custom_domain = setting('AWS_S3_CUSTOM_DOMAIN')
+ calling_format = setting('AWS_S3_CALLING_FORMAT', SubdomainCallingFormat())
+ secure_urls = setting('AWS_S3_SECURE_URLS', True)
+ file_name_charset = setting('AWS_S3_FILE_NAME_CHARSET', 'utf-8')
+ gzip = setting('AWS_IS_GZIPPED', False)
+ preload_metadata = setting('AWS_PRELOAD_METADATA', False)
+ gzip_content_types = setting('GZIP_CONTENT_TYPES', (
+ 'text/css',
+ 'application/javascript',
+ 'application/x-javascript',
+ ))
+ url_protocol = setting('AWS_S3_URL_PROTOCOL', 'http:')
+ host = setting('AWS_S3_HOST', S3Connection.DefaultHost)
+ use_ssl = setting('AWS_S3_USE_SSL', True)
+ port = setting('AWS_S3_PORT', None)
+
+ # The max amount of memory a returned file can take up before being
+ # rolled over into a temporary file on disk. Default is 0: Do not roll over.
+ max_memory_size = setting('AWS_S3_MAX_MEMORY_SIZE', 0)
+
+ def __init__(self, acl=None, bucket=None, **settings):
+ # check if some of the settings we've provided as class attributes
+ # need to be overwritten with values passed in here
+ for name, value in settings.items():
+ if hasattr(self, name):
+ setattr(self, name, value)
+
+ # For backward-compatibility of old differing parameter names
+ if acl is not None:
+ self.default_acl = acl
+ if bucket is not None:
+ self.bucket_name = bucket
+
+ self.location = (self.location or '').lstrip('/')
+ # Backward-compatibility: given the anteriority of the SECURE_URL setting
+ # we fall back to https if specified in order to avoid the construction
+ # of unsecure urls.
+ if self.secure_urls:
+ self.url_protocol = 'https:'
+
+ self._entries = {}
+ self._bucket = None
+ self._connection = None
+
+ if not self.access_key and not self.secret_key:
+ self.access_key, self.secret_key = self._get_access_keys()
+
+ @property
+ def connection(self):
+ if self._connection is None:
+ self._connection = self.connection_class(
+ self.access_key,
+ self.secret_key,
+ is_secure=self.use_ssl,
+ calling_format=self.calling_format,
+ host=self.host,
+ port=self.port,
+ )
+ return self._connection
+
+ @property
+ def bucket(self):
+ """
+ Get the current bucket. If there is no current bucket object
+ create it.
+ """
+ if self._bucket is None:
+ self._bucket = self._get_or_create_bucket(self.bucket_name)
+ return self._bucket
+
+ @property
+ def entries(self):
+ """
+ Get the locally cached files for the bucket.
+ """
+ if self.preload_metadata and not self._entries:
+ self._entries = dict((self._decode_name(entry.key), entry)
+ for entry in self.bucket.list(prefix=self.location))
+ return self._entries
+
+ def _get_access_keys(self):
+ """
+ Gets the access keys to use when accessing S3. If none
+ are provided to the class in the constructor or in the
+ settings then get them from the environment variables.
+ """
+ def lookup_env(names):
+ for name in names:
+ value = os.environ.get(name)
+ if value:
+ return value
+ access_key = self.access_key or lookup_env(self.access_key_names)
+ secret_key = self.secret_key or lookup_env(self.secret_key_names)
+ return access_key, secret_key
+
+ def _get_or_create_bucket(self, name):
+ """
+ Retrieves a bucket if it exists, otherwise creates it.
+ """
+ try:
+ return self.connection.get_bucket(name,
+ validate=self.auto_create_bucket)
+ except self.connection_response_error:
+ if self.auto_create_bucket:
+ bucket = self.connection.create_bucket(name)
+ bucket.set_acl(self.bucket_acl)
+ return bucket
+ raise ImproperlyConfigured("Bucket %s does not exist. Buckets "
+ "can be automatically created by "
+ "setting AWS_AUTO_CREATE_BUCKET to "
+ "``True``." % name)
+
+ def _clean_name(self, name):
+ """
+ Cleans the name so that Windows style paths work
+ """
+ # Normalize Windows style paths
+ clean_name = posixpath.normpath(name).replace('\\', '/')
+
+ # os.path.normpath() can strip trailing slashes so we implement
+ # a workaround here.
+ if name.endswith('/') and not clean_name.endswith('/'):
+ # Add a trailing slash as it was stripped.
+ return clean_name + '/'
+ else:
+ return clean_name
+
+ def _normalize_name(self, name):
+ """
+ Normalizes the name so that paths like /path/to/ignored/../something.txt
+ work. We check to make sure that the path pointed to is not outside
+ the directory specified by the LOCATION setting.
+ """
+ try:
+ return safe_join(self.location, name)
+ except ValueError:
+ raise SuspiciousOperation("Attempted access to '%s' denied." %
+ name)
+
+ def _encode_name(self, name):
+ return smart_str(name, encoding=self.file_name_charset)
+
+ def _decode_name(self, name):
+ return force_unicode(name, encoding=self.file_name_charset)
+
+ def _compress_content(self, content):
+ """Gzip a given string content."""
+ zbuf = StringIO()
+ zfile = GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
+ try:
+ zfile.write(content.read())
+ finally:
+ zfile.close()
+ zbuf.seek(0)
+ content.file = zbuf
+ content.seek(0)
+ return content
+
+ def _open(self, name, mode='rb'):
+ name = self._normalize_name(self._clean_name(name))
+ f = self.file_class(name, mode, self)
+ if not f.key:
+ raise IOError('File does not exist: %s' % name)
+ return f
+
+ def _save(self, name, content):
+ cleaned_name = self._clean_name(name)
+ name = self._normalize_name(cleaned_name)
+ headers = self.headers.copy()
+ content_type = getattr(content, 'content_type',
+ mimetypes.guess_type(name)[0] or self.key_class.DefaultContentType)
+
+ # setting the content_type in the key object is not enough.
+ headers.update({'Content-Type': content_type})
+
+ if self.gzip and content_type in self.gzip_content_types:
+ content = self._compress_content(content)
+ headers.update({'Content-Encoding': 'gzip'})
+
+ content.name = cleaned_name
+ encoded_name = self._encode_name(name)
+ key = self.bucket.get_key(encoded_name)
+ if not key:
+ key = self.bucket.new_key(encoded_name)
+ if self.preload_metadata:
+ self._entries[encoded_name] = key
+
+ key.set_metadata('Content-Type', content_type)
+ self._save_content(key, content, headers=headers)
+ return cleaned_name
+
+ def _save_content(self, key, content, headers):
+ # only pass backwards incompatible arguments if they vary from the default
+ kwargs = {}
+ if self.encryption:
+ kwargs['encrypt_key'] = self.encryption
+ key.set_contents_from_file(content, headers=headers,
+ policy=self.default_acl,
+ reduced_redundancy=self.reduced_redundancy,
+ rewind=True, **kwargs)
+
+ def delete(self, name):
+ name = self._normalize_name(self._clean_name(name))
+ self.bucket.delete_key(self._encode_name(name))
+
+ def exists(self, name):
+ name = self._normalize_name(self._clean_name(name))
+ if self.entries:
+ return name in self.entries
+ k = self.bucket.new_key(self._encode_name(name))
+ return k.exists()
+
+ def listdir(self, name):
+ name = self._normalize_name(self._clean_name(name))
+ # for the bucket.list and logic below name needs to end in /
+ # But for the root path "" we leave it as an empty string
+ if name and not name.endswith('/'):
+ name += '/'
+
+ dirlist = self.bucket.list(self._encode_name(name))
+ files = []
+ dirs = set()
+ base_parts = name.split("/")[:-1]
+ for item in dirlist:
+ parts = item.name.split("/")
+ parts = parts[len(base_parts):]
+ if len(parts) == 1:
+ # File
+ files.append(parts[0])
+ elif len(parts) > 1:
+ # Directory
+ dirs.add(parts[0])
+ return list(dirs), files
+
+ def size(self, name):
+ name = self._normalize_name(self._clean_name(name))
+ if self.entries:
+ entry = self.entries.get(name)
+ if entry:
+ return entry.size
+ return 0
+ return self.bucket.get_key(self._encode_name(name)).size
+
+ def modified_time(self, name):
+ name = self._normalize_name(self._clean_name(name))
+ entry = self.entries.get(name)
+ # only call self.bucket.get_key() if the key is not found
+ # in the preloaded metadata.
+ if entry is None:
+ entry = self.bucket.get_key(self._encode_name(name))
+ # Parse the last_modified string to a local datetime object.
+ return parse_ts_extended(entry.last_modified)
+
+ def url(self, name, headers=None, response_headers=None):
+ # Preserve the trailing slash after normalizing the path.
+ name = self._normalize_name(self._clean_name(name))
+ if self.custom_domain:
+ return "%s//%s/%s" % (self.url_protocol,
+ self.custom_domain, filepath_to_uri(name))
+ return self.connection.generate_url(self.querystring_expire,
+ method='GET', bucket=self.bucket.name, key=self._encode_name(name),
+ headers=headers,
+ query_auth=self.querystring_auth, force_http=not self.secure_urls,
+ response_headers=response_headers)
+
+ def get_available_name(self, name):
+ """ Overwrite existing file with the same name. """
+ if self.file_overwrite:
+ name = self._clean_name(name)
+ return name
+ return super(S3BotoStorage, self).get_available_name(name)
diff --git a/storages/backends/sftpstorage.py b/storages/backends/sftpstorage.py
new file mode 100644
index 0000000..177a637
--- /dev/null
+++ b/storages/backends/sftpstorage.py
@@ -0,0 +1,272 @@
+from __future__ import print_function
+# SFTP storage backend for Django.
+# Author: Brent Tubbs <brent.tubbs@gmail.com>
+# License: MIT
+#
+# Modeled on the FTP storage by Rafal Jonca <jonca.rafal@gmail.com>
+#
+# Settings:
+#
+# SFTP_STORAGE_HOST - The hostname where you want the files to be saved.
+#
+# SFTP_STORAGE_ROOT - The root directory on the remote host into which files
+# should be placed. Should work the same way that STATIC_ROOT works for local
+# files. Must include a trailing slash.
+#
+# SFTP_STORAGE_PARAMS (Optional) - A dictionary containing connection
+# parameters to be passed as keyword arguments to
+# paramiko.SSHClient().connect() (do not include hostname here). See
+# http://www.lag.net/paramiko/docs/paramiko.SSHClient-class.html#connect for
+# details
+#
+# SFTP_STORAGE_INTERACTIVE (Optional) - A boolean indicating whether to prompt
+# for a password if the connection cannot be made using keys, and there is not
+# already a password in SFTP_STORAGE_PARAMS. You can set this to True to
+# enable interactive login when running 'manage.py collectstatic', for example.
+#
+# DO NOT set SFTP_STORAGE_INTERACTIVE to True if you are using this storage
+# for files being uploaded to your site by users, because you'll have no way
+# to enter the password when they submit the form..
+#
+# SFTP_STORAGE_FILE_MODE (Optional) - A bitmask for setting permissions on
+# newly-created files. See http://docs.python.org/library/os.html#os.chmod for
+# acceptable values.
+#
+# SFTP_STORAGE_DIR_MODE (Optional) - A bitmask for setting permissions on
+# newly-created directories. See
+# http://docs.python.org/library/os.html#os.chmod for acceptable values.
+#
+# Hint: if you start the mode number with a 0 you can express it in octal
+# just like you would when doing "chmod 775 myfile" from bash.
+#
+# SFTP_STORAGE_UID (Optional) - uid of the account that should be set as owner
+# of the files on the remote host. You have to be root to set this.
+#
+# SFTP_STORAGE_GID (Optional) - gid of the group that should be set on the
+# files on the remote host. You have to be a member of the group to set this.
+# SFTP_KNOWN_HOST_FILE (Optional) - absolute path of know host file, if it isn't
+# set "~/.ssh/known_hosts" will be used
+
+
+import getpass
+import os
+import paramiko
+import posixpath
+import stat
+import urlparse
+from datetime import datetime
+
+from django.conf import settings
+from django.core.files.base import File
+from django.core.files.storage import Storage
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO # noqa
+
+
+class SFTPStorage(Storage):
+
+ def __init__(self):
+ self._host = settings.SFTP_STORAGE_HOST
+
+ # if present, settings.SFTP_STORAGE_PARAMS should be a dict with params
+ # matching the keyword arguments to paramiko.SSHClient().connect(). So
+ # you can put username/password there. Or you can omit all that if
+ # you're using keys.
+ self._params = getattr(settings, 'SFTP_STORAGE_PARAMS', {})
+ self._interactive = getattr(settings, 'SFTP_STORAGE_INTERACTIVE',
+ False)
+ self._file_mode = getattr(settings, 'SFTP_STORAGE_FILE_MODE', None)
+ self._dir_mode = getattr(settings, 'SFTP_STORAGE_DIR_MODE', None)
+
+ self._uid = getattr(settings, 'SFTP_STORAGE_UID', None)
+ self._gid = getattr(settings, 'SFTP_STORAGE_GID', None)
+ self._known_host_file = getattr(settings, 'SFTP_KNOWN_HOST_FILE', None)
+
+ self._root_path = settings.SFTP_STORAGE_ROOT
+ self._base_url = settings.MEDIA_URL
+
+ # for now it's all posix paths. Maybe someday we'll support figuring
+ # out if the remote host is windows.
+ self._pathmod = posixpath
+
+ def _connect(self):
+ self._ssh = paramiko.SSHClient()
+
+ if self._known_host_file is not None:
+ self._ssh.load_host_keys(self._known_host_file)
+ else:
+ # automatically add host keys from current user.
+ self._ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts")))
+
+ # and automatically add new host keys for hosts we haven't seen before.
+ self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+ try:
+ self._ssh.connect(self._host, **self._params)
+ except paramiko.AuthenticationException as e:
+ if self._interactive and 'password' not in self._params:
+ # If authentication has failed, and we haven't already tried
+ # username/password, and configuration allows it, then try
+ # again with username/password.
+ if 'username' not in self._params:
+ self._params['username'] = getpass.getuser()
+ self._params['password'] = getpass.getpass()
+ self._connect()
+ else:
+ raise paramiko.AuthenticationException(e)
+ except Exception as e:
+ print(e)
+
+ if not hasattr(self, '_sftp'):
+ self._sftp = self._ssh.open_sftp()
+
+ @property
+ def sftp(self):
+ """Lazy SFTP connection"""
+ if not hasattr(self, '_sftp'):
+ self._connect()
+ return self._sftp
+
+ def _join(self, *args):
+ # Use the path module for the remote host type to join a path together
+ return self._pathmod.join(*args)
+
+ def _remote_path(self, name):
+ return self._join(self._root_path, name)
+
+ def _open(self, name, mode='rb'):
+ return SFTPStorageFile(name, self, mode)
+
+ def _read(self, name):
+ remote_path = self._remote_path(name)
+ return self.sftp.open(remote_path, 'rb')
+
+ def _chown(self, path, uid=None, gid=None):
+ """Set uid and/or gid for file at path."""
+ # Paramiko's chown requires both uid and gid, so look them up first if
+ # we're only supposed to set one.
+ if uid is None or gid is None:
+ attr = self.sftp.stat(path)
+ uid = uid or attr.st_uid
+ gid = gid or attr.st_gid
+ self.sftp.chown(path, uid, gid)
+
+ def _mkdir(self, path):
+ """Create directory, recursing up to create parent dirs if
+ necessary."""
+ parent = self._pathmod.dirname(path)
+ if not self.exists(parent):
+ self._mkdir(parent)
+ self.sftp.mkdir(path)
+
+ if self._dir_mode is not None:
+ self.sftp.chmod(path, self._dir_mode)
+
+ if self._uid or self._gid:
+ self._chown(path, uid=self._uid, gid=self._gid)
+
+ def _save(self, name, content):
+ """Save file via SFTP."""
+ content.open()
+ path = self._remote_path(name)
+ dirname = self._pathmod.dirname(path)
+ if not self.exists(dirname):
+ self._mkdir(dirname)
+
+ f = self.sftp.open(path, 'wb')
+ f.write(content.file.read())
+ f.close()
+
+ # set file permissions if configured
+ if self._file_mode is not None:
+ self.sftp.chmod(path, self._file_mode)
+ if self._uid or self._gid:
+ self._chown(path, uid=self._uid, gid=self._gid)
+ return name
+
+ def delete(self, name):
+ remote_path = self._remote_path(name)
+ self.sftp.remove(remote_path)
+
+ def exists(self, name):
+ # Try to retrieve file info. Return true on success, false on failure.
+ remote_path = self._remote_path(name)
+ try:
+ self.sftp.stat(remote_path)
+ return True
+ except IOError:
+ return False
+
+ def _isdir_attr(self, item):
+ # Return whether an item in sftp.listdir_attr results is a directory
+ if item.st_mode is not None:
+ return stat.S_IFMT(item.st_mode) == stat.S_IFDIR
+ else:
+ return False
+
+ def listdir(self, path):
+ remote_path = self._remote_path(path)
+ dirs, files = [], []
+ for item in self.sftp.listdir_attr(remote_path):
+ if self._isdir_attr(item):
+ dirs.append(item.filename)
+ else:
+ files.append(item.filename)
+ return dirs, files
+
+ def size(self, name):
+ remote_path = self._remote_path(name)
+ return self.sftp.stat(remote_path).st_size
+
+ def accessed_time(self, name):
+ remote_path = self._remote_path(name)
+ utime = self.sftp.stat(remote_path).st_atime
+ return datetime.fromtimestamp(utime)
+
+ def modified_time(self, name):
+ remote_path = self._remote_path(name)
+ utime = self.sftp.stat(remote_path).st_mtime
+ return datetime.fromtimestamp(utime)
+
+ def url(self, name):
+ if self._base_url is None:
+ raise ValueError("This file is not accessible via a URL.")
+ return urlparse.urljoin(self._base_url, name).replace('\\', '/')
+
+
+class SFTPStorageFile(File):
+ def __init__(self, name, storage, mode):
+ self._name = name
+ self._storage = storage
+ self._mode = mode
+ self._is_dirty = False
+ self.file = StringIO()
+ self._is_read = False
+
+ @property
+ def size(self):
+ if not hasattr(self, '_size'):
+ self._size = self._storage.size(self._name)
+ return self._size
+
+ def read(self, num_bytes=None):
+ if not self._is_read:
+ self.file = self._storage._read(self._name)
+ self._is_read = True
+
+ return self.file.read(num_bytes)
+
+ def write(self, content):
+ if 'w' not in self._mode:
+ raise AttributeError("File was opened for read-only access.")
+ self.file = StringIO(content)
+ self._is_dirty = True
+ self._is_read = True
+
+ def close(self):
+ if self._is_dirty:
+ self._storage._save(self._name, self.file.getvalue())
+ self.file.close()
diff --git a/storages/backends/symlinkorcopy.py b/storages/backends/symlinkorcopy.py
new file mode 100644
index 0000000..14f8e29
--- /dev/null
+++ b/storages/backends/symlinkorcopy.py
@@ -0,0 +1,62 @@
+import os
+
+from django.conf import settings
+from django.core.files.storage import FileSystemStorage
+
+__doc__ = """
+I needed to efficiently create a mirror of a directory tree (so that
+"origin pull" CDNs can automatically pull files). The trick was that
+some files could be modified, and some could be identical to the original.
+Of course it doesn't make sense to store the exact same data twice on the
+file system. So I created SymlinkOrCopyStorage.
+
+SymlinkOrCopyStorage allows you to symlink a file when it's identical to
+the original file and to copy the file if it's modified.
+Of course, it's impossible to know if a file is modified just by looking
+at the file, without knowing what the original file was.
+That's what the symlinkWithin parameter is for. It accepts one or more paths
+(if multiple, they should be concatenated using a colon (:)).
+Files that will be saved using SymlinkOrCopyStorage are then checked on their
+location: if they are within one of the symlink_within directories,
+they will be symlinked, otherwise they will be copied.
+
+The rationale is that unmodified files will exist in their original location,
+e.g. /htdocs/example.com/image.jpg and modified files will be stored in
+a temporary directory, e.g. /tmp/image.jpg.
+"""
+
+class SymlinkOrCopyStorage(FileSystemStorage):
+ """Stores symlinks to files instead of actual files whenever possible
+
+ When a file that's being saved is currently stored in the symlink_within
+ directory, then symlink the file. Otherwise, copy the file.
+ """
+ def __init__(self, location=settings.MEDIA_ROOT, base_url=settings.MEDIA_URL,
+ symlink_within=None):
+ super(SymlinkOrCopyStorage, self).__init__(location, base_url)
+ self.symlink_within = symlink_within.split(":")
+
+ def _save(self, name, content):
+ full_path_dst = self.path(name)
+
+ directory = os.path.dirname(full_path_dst)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ elif not os.path.isdir(directory):
+ raise IOError("%s exists and is not a directory." % directory)
+
+ full_path_src = os.path.abspath(content.name)
+
+ symlinked = False
+ # Only symlink if the current platform supports it.
+ if getattr(os, "symlink", False):
+ for path in self.symlink_within:
+ if full_path_src.startswith(path):
+ os.symlink(full_path_src, full_path_dst)
+ symlinked = True
+ break
+
+ if not symlinked:
+ super(SymlinkOrCopyStorage, self)._save(name, content)
+
+ return name