diff --git a/azure-storage/azure/storage/constants.py b/azure-storage/azure/storage/constants.py index 9fb4884eb757..d60013f6cf74 100644 --- a/azure-storage/azure/storage/constants.py +++ b/azure-storage/azure/storage/constants.py @@ -22,11 +22,13 @@ # Live ServiceClient URLs BLOB_SERVICE_HOST_BASE = '.blob.core.windows.net' +FILES_SERVICE_HOST_BASE = '.file.core.windows.net' QUEUE_SERVICE_HOST_BASE = '.queue.core.windows.net' TABLE_SERVICE_HOST_BASE = '.table.core.windows.net' # Development ServiceClient URLs DEV_BLOB_HOST = '127.0.0.1:10000' +DEV_FILES_HOST = '127.0.0.1:10003' DEV_QUEUE_HOST = '127.0.0.1:10001' DEV_TABLE_HOST = '127.0.0.1:10002' diff --git a/azure-storage/azure/storage/files/__init__.py b/azure-storage/azure/storage/files/__init__.py new file mode 100644 index 000000000000..82da64a3472d --- /dev/null +++ b/azure-storage/azure/storage/files/__init__.py @@ -0,0 +1,26 @@ +#------------------------------------------------------------------------- +# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#-------------------------------------------------------------------------- +from ..constants import ( + FILES_SERVICE_HOST_BASE, + DEV_FILES_HOST +) + +from .models import ( + ShareEnumResults, + Share, + Properties +) + +from .filesservice import FilesService diff --git a/azure-storage/azure/storage/files/_serialization.py b/azure-storage/azure/storage/files/_serialization.py new file mode 100644 index 000000000000..dcf234e84fa5 --- /dev/null +++ b/azure-storage/azure/storage/files/_serialization.py @@ -0,0 +1,46 @@ +#------------------------------------------------------------------------- +# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#-------------------------------------------------------------------------- +from time import time +from wsgiref.handlers import format_date_time +from .._common_serialization import ( + _parse_response_for_dict, + ETree, + _ETreeXmlToObject +) +from .._common_conversion import ( + _decode_base64_to_text, + _encode_base64 +) +from .._serialization import _update_storage_header +from .models import ( + ShareEnumResults, + Share +) + +def _update_storage_files_header(request, authentication): + request = _update_storage_header(request) + current_time = format_date_time(time()) + request.headers.append(('x-ms-date', current_time)) + request.headers.append( + ('Content-Type', 'application/octet-stream Charset=UTF-8')) + authentication.sign_request(request) + + return request.headers + +def _parse_files_enum_results_list(response): + respbody = response.body + return _ETreeXmlToObject.parse_enum_results_list( + response, ShareEnumResults, "Shares", Share + ) diff --git a/azure-storage/azure/storage/files/filesservice.py b/azure-storage/azure/storage/files/filesservice.py new file mode 100644 index 000000000000..19a1bac9d9cf --- /dev/null +++ b/azure-storage/azure/storage/files/filesservice.py @@ -0,0 +1,215 @@ +#------------------------------------------------------------------------- +# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#-------------------------------------------------------------------------- +from .._common_error import ( + _validate_not_none +) +from .._common_serialization import ( + _update_request_uri_query_local_storage +) +from .._common_conversion import ( + _encode_base64, + _int_or_none, + _str, + _str_or_none, +) +from .._http import HTTPRequest +from .models import ( + Share, + ShareEnumResults +) +from ..auth import ( + StorageSASAuthentication, + StorageSharedKeyAuthentication, + StorageNoAuthentication, +) +from ..connection import ( + StorageConnectionParameters, +) +from ..constants import ( + FILES_SERVICE_HOST_BASE, + DEFAULT_HTTP_TIMEOUT, + DEV_FILES_HOST, + X_MS_VERSION, +) +from ._serialization import ( + _parse_files_enum_results_list, + _update_storage_files_header, +) +from ..sharedaccesssignature import ( + SharedAccessSignature, + ResourceType, +) +from ..storageclient import _StorageClient + +class FilesService(_StorageClient): + + ''' + This is the main class managing Files resources. + ''' + + def __init__( + self, account_name=None, account_key=None, protocol='https', + host_base=FILES_SERVICE_HOST_BASE, dev_host=DEV_FILES_HOST, + timeout=DEFAULT_HTTP_TIMEOUT, sas_token=None, connection_string=None, + request_session=None + ): + ''' + account_name: + your storage account name, required for all operations. + account_key: + your storage account key, required for all operations. + protocol: + Optional. Protocol. Defaults to https. + host_base: + Optional. Live host base url. Defaults to Azure url. Override this + for on-premise. + dev_host: + Optional. Dev host url. Defaults to localhost. + timeout: + Optional. Timeout for the http request, in seconds. + sas_token: + Optional. Token to use to authenticate with shared access signature. + connection_string: + Optional. If specified, the first four parameters + (account_name, account_key, protocol, host_base) may be overridden + by values specified in the connection_string. The next three + parameters (dev_host, timeout, sas_token) cannot be specified + with a connection_string. See http://azure.microsoft.com/ \ + en-us/documentation/articles/storage-configure-connection-string + for the connection string format. + request_session: + Optional. Session object to use for http requests. If this is + specified, it replaces the default use of httplib. + ''' + if connection_string is not None: + connection_params = StorageConnectionParameters(connection_string) + account_name = connection_params.account_name + account_key = connection_params.account_key + protocol = connection_params.protocol.lower() + host_base = connection_params.host_base_blob + + super(FilesService, self).__init__( + account_name, account_key, protocol, + host_base, dev_host, timeout, sas_token, request_session + ) + + if self.account_key: + self.authentication = StorageSharedKeyAuthentication( + self.account_name, + self.account_key, + ) + elif self.sas_token: + self.authentication = StorageSASAuthentication(self.sas_token) + else: + self.authentication = StorageNoAuthentication() + + def list_shares( + self, prefix=None, marker=None, maxresults=None, include=None + ): + ''' + The List Shares operation returns a list of the shares under the + specified account. + + prefix: + Optional. Filters the results to return only shares whose names + begin with the specified prefix. + marker: + Optional. A string value that identifies the portion of the list to + be returned with the next list operation. + maxresults: + Optional. Specifies the maximum number of shares to return. + include: + Optional. Include this parameter to specify that the share's + metadata be returned as part of the response body. set this + parameter to string 'metadata' to get shares's metadata. + ''' + request = HTTPRequest() + request.method = 'GET' + request.host = self._get_host() + request.path = '/?comp=list' + request.query = [ + ('prefix', _str_or_none(prefix)), + ('marker', _str_or_none(marker)), + ('maxresults', _int_or_none(maxresults)), + ('include', _str_or_none(include)) + ] + request.path, request.query = _update_request_uri_query_local_storage( + request, self.use_local_storage + ) + request.headers = _update_storage_files_header( + request, self.authentication + ) + response = self._perform_request(request) + + return _parse_files_enum_results_list(response) + + def create_share(self, share_name, x_ms_meta_name_values=None): + ''' + The Create Share operation creates a new share under the specified + account. If the share with the same name already exists, the operation + fails. The share resource includes metadata and properties for that + share. It does not include a list of the files contained by the share. + + share_name: + Name of the share to create + x_ms_meta_name_values: + Optional. A dict with name_value pairs to associate with the + share as metadata. Example:{'Category':'test'} + ''' + _validate_not_none('share_name', share_name) + request = HTTPRequest() + request.method = 'PUT' + request.host = self._get_host() + request.path = '/' + _str(share_name) + '?restype=share' + request.headers = [ + ('x-ms-meta-name-values', x_ms_meta_name_values) + ] + request.path, request.query = _update_request_uri_query_local_storage( + request, self.use_local_storage + ) + request.headers = _update_storage_files_header( + request, self.authentication + ) + self._perform_request(request) + return True + + def delete_share(self, share_name, x_ms_meta_name_values=None): + ''' + The Delete Share operation marks the specified share for deletion. + The share and any files contained within it are later deleted + during garbage collection. + + share_name: + Name of the share to create + x_ms_meta_name_values: + Optional. A dict with name_value pairs to associate with the + share as metadata. Example:{'Category':'test'} + ''' + _validate_not_none('share_name', share_name) + request = HTTPRequest() + request.method = 'DELETE' + request.host = self._get_host() + request.path = '/' + _str(share_name) + '?restype=share' + request.headers = [ + ('x-ms-meta-name-values', x_ms_meta_name_values) + ] + request.path, request.query = _update_request_uri_query_local_storage( + request, self.use_local_storage + ) + request.headers = _update_storage_files_header( + request, self.authentication + ) + self._perform_request(request) + return True diff --git a/azure-storage/azure/storage/files/models.py b/azure-storage/azure/storage/files/models.py new file mode 100644 index 000000000000..2d0c9297b1b0 --- /dev/null +++ b/azure-storage/azure/storage/files/models.py @@ -0,0 +1,58 @@ +#------------------------------------------------------------------------- +# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#-------------------------------------------------------------------------- +from .._common_models import ( + WindowsAzureData, + _list_of +) +from ..models import ( + EnumResultsBase +) + +class ShareEnumResults(EnumResultsBase): + + ''' File Share list. ''' + + def __init__(self): + EnumResultsBase.__init__(self) + self.shares = _list_of(Share) + + def __iter__(self): + return iter(self.shares) + + def __len__(self): + return len(self.shares) + + def __getitem__(self, index): + return self.shares[index] + + +class Share(WindowsAzureData): + + ''' File Share class. ''' + + def __init__(self): + self.name = u'' + self.url = u'' + self.properties = Properties() + self.metadata = {} + + +class Properties(WindowsAzureData): + + ''' File Share's properties class. ''' + + def __init__(self): + self.last_modified = u'' + self.etag = u'' diff --git a/azure-storage/setup.py b/azure-storage/setup.py index 8777718b68a1..25a9c1cd16d3 100644 --- a/azure-storage/setup.py +++ b/azure-storage/setup.py @@ -43,6 +43,7 @@ 'azure.storage', 'azure.storage._http', 'azure.storage.blob', + 'azure.storage.files', 'azure.storage.queue', 'azure.storage.table', ], diff --git a/azure-storage/tests/test_storage_files.py b/azure-storage/tests/test_storage_files.py new file mode 100644 index 000000000000..3988951bb4f9 --- /dev/null +++ b/azure-storage/tests/test_storage_files.py @@ -0,0 +1,112 @@ +# coding: utf-8 +#------------------------------------------------------------------------- +# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#-------------------------------------------------------------------------- +import base64 +import datetime +import os +import random +import sys +import time +import unittest + +from azure.common import ( + WindowsAzureError, + WindowsAzureConflictError, + WindowsAzureMissingResourceError +) +from azure.storage import ( + DEV_ACCOUNT_NAME, + DEV_ACCOUNT_KEY, + AccessPolicy, + Logging, + HourMetrics, + MinuteMetrics, + SharedAccessPolicy, + SignedIdentifier, + SignedIdentifiers, + StorageServiceProperties +) +from azure.storage.files import ( + FILES_SERVICE_HOST_BASE, + FilesService +) +from azure.storage.storageclient import ( + AZURE_STORAGE_ACCESS_KEY, + AZURE_STORAGE_ACCOUNT, + EMULATED, +) +from testutils.common_recordingtestcase import ( + TestMode, + record, +) +from tests.storage_testcase import StorageTestCase + +#------------------------------------------------------------------------------ + + +class FilesServiceTest(StorageTestCase): + + def setUp(self): + super(FilesServiceTest, self).setUp() + + if self.settings.REMOTE_STORAGE_ACCOUNT_NAME and self.settings.REMOTE_STORAGE_ACCOUNT_KEY: + self.fs = self._create_storage_service( + FilesService, + self.settings, + self.settings.REMOTE_STORAGE_ACCOUNT_NAME, + self.settings.REMOTE_STORAGE_ACCOUNT_KEY, + ) + else: + print("REMOTE_STORAGE_ACCOUNT_NAME and REMOTE_STORAGE_ACCOUNT_KEY not set in test settings file.") + + self.sharename = 'testshare' + self.timeout = 1 + self.trycount = 60 + + def tearDown(self): + return super(FilesServiceTest, self).tearDown() + + def test_create_and_list_share(self): + try_count = 0 + while try_count < self.trycount: + try: + created = self.fs.create_share(self.sharename) + self.assertTrue(created) + try_count = self.trycount + except: + time.sleep(self.timeout) + try_count = try_count + 1 + share_name = None + try_count = 0 + while try_count < self.trycount: + try: + for share in self.fs.list_shares(): + if share.name == self.sharename: + share_name = share.name + try_count = self.trycount + break + except: + time.sleep(self.timeout) + try_count = try_count + 1 + self.assertEqual(share_name, self.sharename) + + def test_delete_share(self): + deleted = self.fs.delete_share(self.sharename) + self.assertTrue(deleted) + + +#------------------------------------------------------------------------------ +if __name__ == '__main__': + unittest.main()