268 lines
8.6 KiB
Python
268 lines
8.6 KiB
Python
from __future__ import absolute_import
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
from time import sleep
|
|
import os.path as op
|
|
|
|
try:
|
|
from azure.storage.blob import BlobPermissions
|
|
from azure.storage.blob import BlockBlobService
|
|
except ImportError:
|
|
BlobPermissions = BlockBlobService = None
|
|
|
|
from flask import redirect
|
|
|
|
from . import BaseFileAdmin
|
|
|
|
|
|
class AzureStorage(object):
|
|
"""
|
|
Storage object representing files on an Azure Storage container.
|
|
|
|
Usage::
|
|
|
|
from flask_admin.contrib.fileadmin import BaseFileAdmin
|
|
from flask_admin.contrib.fileadmin.azure import AzureStorage
|
|
|
|
class MyAzureAdmin(BaseFileAdmin):
|
|
# Configure your class however you like
|
|
pass
|
|
|
|
fileadmin_view = MyAzureAdmin(storage=AzureStorage(...))
|
|
|
|
"""
|
|
_fakedir = '.dir'
|
|
_copy_poll_interval_seconds = 1
|
|
_send_file_lookback = timedelta(minutes=15)
|
|
_send_file_validity = timedelta(hours=1)
|
|
separator = '/'
|
|
|
|
def __init__(self, container_name, connection_string):
|
|
"""
|
|
Constructor
|
|
|
|
:param container_name:
|
|
Name of the container that the files are on.
|
|
|
|
:param connection_string:
|
|
Azure Blob Storage Connection String
|
|
"""
|
|
|
|
if not BlockBlobService:
|
|
raise ValueError('Could not import Azure Blob Storage SDK. '
|
|
'You can install the SDK using '
|
|
'pip install azure-storage-blob')
|
|
|
|
self._container_name = container_name
|
|
self._connection_string = connection_string
|
|
self.__client = None
|
|
|
|
@property
|
|
def _client(self):
|
|
if not self.__client:
|
|
self.__client = BlockBlobService(
|
|
connection_string=self._connection_string)
|
|
self.__client.create_container(
|
|
self._container_name, fail_on_exist=False)
|
|
return self.__client
|
|
|
|
@classmethod
|
|
def _get_blob_last_modified(cls, blob):
|
|
last_modified = blob.properties.last_modified
|
|
tzinfo = last_modified.tzinfo
|
|
epoch = last_modified - datetime(1970, 1, 1, tzinfo=tzinfo)
|
|
return epoch.total_seconds()
|
|
|
|
@classmethod
|
|
def _ensure_blob_path(cls, path):
|
|
if path is None:
|
|
return None
|
|
|
|
path_parts = path.split(op.sep)
|
|
return cls.separator.join(path_parts).lstrip(cls.separator)
|
|
|
|
def get_files(self, path, directory):
|
|
if directory and path != directory:
|
|
path = op.join(path, directory)
|
|
|
|
path = self._ensure_blob_path(path)
|
|
directory = self._ensure_blob_path(directory)
|
|
|
|
path_parts = path.split(self.separator) if path else []
|
|
num_path_parts = len(path_parts)
|
|
folders = set()
|
|
files = []
|
|
|
|
for blob in self._client.list_blobs(self._container_name, path):
|
|
blob_path_parts = blob.name.split(self.separator)
|
|
name = blob_path_parts.pop()
|
|
|
|
blob_is_file_at_current_level = blob_path_parts == path_parts
|
|
blob_is_directory_file = name == self._fakedir
|
|
|
|
if blob_is_file_at_current_level and not blob_is_directory_file:
|
|
rel_path = blob.name
|
|
is_dir = False
|
|
size = blob.properties.content_length
|
|
last_modified = self._get_blob_last_modified(blob)
|
|
files.append((name, rel_path, is_dir, size, last_modified))
|
|
else:
|
|
next_level_folder = blob_path_parts[:num_path_parts + 1]
|
|
folder_name = self.separator.join(next_level_folder)
|
|
folders.add(folder_name)
|
|
|
|
folders.discard(directory)
|
|
for folder in folders:
|
|
name = folder.split(self.separator)[-1]
|
|
rel_path = folder
|
|
is_dir = True
|
|
size = 0
|
|
last_modified = 0
|
|
files.append((name, rel_path, is_dir, size, last_modified))
|
|
|
|
return files
|
|
|
|
def is_dir(self, path):
|
|
path = self._ensure_blob_path(path)
|
|
|
|
num_blobs = 0
|
|
for blob in self._client.list_blobs(self._container_name, path):
|
|
blob_path_parts = blob.name.split(self.separator)
|
|
is_explicit_directory = blob_path_parts[-1] == self._fakedir
|
|
if is_explicit_directory:
|
|
return True
|
|
|
|
num_blobs += 1
|
|
path_cannot_be_leaf = num_blobs >= 2
|
|
if path_cannot_be_leaf:
|
|
return True
|
|
|
|
return False
|
|
|
|
def path_exists(self, path):
|
|
path = self._ensure_blob_path(path)
|
|
|
|
if path == self.get_base_path():
|
|
return True
|
|
|
|
try:
|
|
next(iter(self._client.list_blobs(self._container_name, path)))
|
|
except StopIteration:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def get_base_path(self):
|
|
return ''
|
|
|
|
def get_breadcrumbs(self, path):
|
|
path = self._ensure_blob_path(path)
|
|
|
|
accumulator = []
|
|
breadcrumbs = []
|
|
for folder in path.split(self.separator):
|
|
accumulator.append(folder)
|
|
breadcrumbs.append((folder, self.separator.join(accumulator)))
|
|
return breadcrumbs
|
|
|
|
def send_file(self, file_path):
|
|
file_path = self._ensure_blob_path(file_path)
|
|
|
|
if not self._client.exists(self._container_name, file_path):
|
|
raise ValueError()
|
|
|
|
now = datetime.utcnow()
|
|
url = self._client.make_blob_url(self._container_name, file_path)
|
|
sas = self._client.generate_blob_shared_access_signature(
|
|
self._container_name, file_path,
|
|
BlobPermissions.READ,
|
|
expiry=now + self._send_file_validity,
|
|
start=now - self._send_file_lookback)
|
|
return redirect('%s?%s' % (url, sas))
|
|
|
|
def read_file(self, path):
|
|
path = self._ensure_blob_path(path)
|
|
|
|
blob = self._client.get_blob_to_bytes(self._container_name, path)
|
|
return blob.content
|
|
|
|
def write_file(self, path, content):
|
|
path = self._ensure_blob_path(path)
|
|
|
|
self._client.create_blob_from_text(self._container_name, path, content)
|
|
|
|
def save_file(self, path, file_data):
|
|
path = self._ensure_blob_path(path)
|
|
|
|
self._client.create_blob_from_stream(self._container_name, path,
|
|
file_data.stream)
|
|
|
|
def delete_tree(self, directory):
|
|
directory = self._ensure_blob_path(directory)
|
|
|
|
for blob in self._client.list_blobs(self._container_name, directory):
|
|
self._client.delete_blob(self._container_name, blob.name)
|
|
|
|
def delete_file(self, file_path):
|
|
file_path = self._ensure_blob_path(file_path)
|
|
|
|
self._client.delete_blob(self._container_name, file_path)
|
|
|
|
def make_dir(self, path, directory):
|
|
path = self._ensure_blob_path(path)
|
|
directory = self._ensure_blob_path(directory)
|
|
|
|
blob = self.separator.join([path, directory, self._fakedir])
|
|
blob = blob.lstrip(self.separator)
|
|
self._client.create_blob_from_text(self._container_name, blob, '')
|
|
|
|
def _copy_blob(self, src, dst):
|
|
src_url = self._client.make_blob_url(self._container_name, src)
|
|
copy = self._client.copy_blob(self._container_name, dst, src_url)
|
|
while copy.status != 'success':
|
|
sleep(self._copy_poll_interval_seconds)
|
|
copy = self._client.get_blob_properties(
|
|
self._container_name, dst).properties.copy
|
|
|
|
def _rename_file(self, src, dst):
|
|
self._copy_blob(src, dst)
|
|
self.delete_file(src)
|
|
|
|
def _rename_directory(self, src, dst):
|
|
for blob in self._client.list_blobs(self._container_name, src):
|
|
self._rename_file(blob.name, blob.name.replace(src, dst, 1))
|
|
|
|
def rename_path(self, src, dst):
|
|
src = self._ensure_blob_path(src)
|
|
dst = self._ensure_blob_path(dst)
|
|
|
|
if self.is_dir(src):
|
|
self._rename_directory(src, dst)
|
|
else:
|
|
self._rename_file(src, dst)
|
|
|
|
|
|
class AzureFileAdmin(BaseFileAdmin):
|
|
"""
|
|
Simple Azure Blob Storage file-management interface.
|
|
|
|
:param container_name:
|
|
Name of the container that the files are on.
|
|
|
|
:param connection_string:
|
|
Azure Blob Storage Connection String
|
|
|
|
Sample usage::
|
|
|
|
from flask_admin import Admin
|
|
from flask_admin.contrib.fileadmin.azure import AzureFileAdmin
|
|
|
|
admin = Admin()
|
|
|
|
admin.add_view(AzureFileAdmin('files_container', 'my-connection-string')
|
|
"""
|
|
|
|
def __init__(self, container_name, connection_string, *args, **kwargs):
|
|
storage = AzureStorage(container_name, connection_string)
|
|
super(AzureFileAdmin, self).__init__(*args, storage=storage, **kwargs)
|