This commit is contained in:
Fred Park 2017-01-12 14:44:44 -08:00
Родитель 524fe56b4e
Коммит 97ac6df710
14 изменённых файлов: 492 добавлений и 4911 удалений

Просмотреть файл

@ -6,6 +6,7 @@ omit =
exclude_lines = exclude_lines =
# Have to re-enable the standard pragma # Have to re-enable the standard pragma
pragma: no cover pragma: no cover
noqa
# Don't complain about missing debug-only code: # Don't complain about missing debug-only code:
def __repr__ def __repr__

1
.gitignore поставляемый
Просмотреть файл

@ -43,6 +43,7 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*,cover *,cover
junit-*.xml
# Translations # Translations
*.mo *.mo

Просмотреть файл

@ -5,6 +5,7 @@ python:
- 3.3 - 3.3
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6
- pypy - pypy
# disable pypy3 until 3.3 compliance # disable pypy3 until 3.3 compliance
#- pypy3 #- pypy3

18
README.md Normal file
Просмотреть файл

@ -0,0 +1,18 @@
blobxfer
========
AzCopy-like OS independent Azure storage blob and file share transfer tool
Change Log
----------
See the [CHANGELOG.md](https://github.com/Azure/blobxfer/blob/master/CHANGELOG.md) file.
------------------------------------------------------------------------
This project has adopted the
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the
[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [<opencode@microsoft.com>](mailto:opencode@microsoft.com) with any
additional questions or comments.

Просмотреть файл

@ -1,426 +0,0 @@
.. image:: https://travis-ci.org/Azure/blobxfer.svg?branch=master
:target: https://travis-ci.org/Azure/blobxfer
.. image:: https://coveralls.io/repos/github/Azure/blobxfer/badge.svg?branch=master
:target: https://coveralls.io/github/Azure/blobxfer?branch=master
.. image:: https://img.shields.io/pypi/v/blobxfer.svg
:target: https://pypi.python.org/pypi/blobxfer
.. image:: https://img.shields.io/pypi/pyversions/blobxfer.svg
:target: https://pypi.python.org/pypi/blobxfer
.. image:: https://img.shields.io/pypi/l/blobxfer.svg
:target: https://pypi.python.org/pypi/blobxfer
.. image:: https://img.shields.io/docker/pulls/alfpark/blobxfer.svg
:target: https://hub.docker.com/r/alfpark/blobxfer
.. image:: https://images.microbadger.com/badges/image/alfpark/blobxfer.svg
:target: https://microbadger.com/images/alfpark/blobxfer
blobxfer
========
AzCopy-like OS independent Azure storage blob and file share transfer tool
Installation
------------
`blobxfer`_ is on PyPI and can be installed via:
::
pip install blobxfer
blobxfer is compatible with Python 2.7 and 3.3+. To install for Python 3, some
distributions may use ``pip3`` instead. If you do not want to install blobxfer
as a system-wide binary and modify system-wide python packages, use the
``--user`` flag with ``pip`` or ``pip3``.
blobxfer is also on `Docker Hub`_, and the Docker image for Linux can be
pulled with the following command:
::
docker pull alfpark/blobxfer
Please see example usage below on how to use the docker image.
If you encounter difficulties installing the script, it may be due to the
``cryptography`` dependency. Please ensure that your system is able to install
binary wheels provided by these dependencies (e.g., on Windows) or is able to
compile the dependencies (i.e., ensure you have a C compiler, python, ssl,
and ffi development libraries/headers installed prior to invoking pip). For
instance, to install blobxfer on a fresh Ubuntu 14.04/16.04 installation for
Python 2.7, issue the following commands:
::
apt-get update
apt-get install -y build-essential libssl-dev libffi-dev libpython-dev python-dev python-pip
pip install --upgrade blobxfer
If you need more fine-grained control on installing dependencies, continue
reading this section. Depending upon the desired mode of authentication with
Azure and options, the script will require the following packages, some of
which will automatically pull required dependent packages. Below is a list of
dependent packages:
- Base Requirements
- `azure-common`_
- `azure-storage`_
- `requests`_
- Encryption Support
- `cryptography`_
- Service Management Certificate Support
- `azure-servicemanagement-legacy`_
You can install these packages using pip, easy_install or through standard
setup.py procedures. These dependencies will be automatically installed if
using a package-based install or setup.py. The required versions of these
dependent packages can be found in ``setup.py``.
.. _blobxfer: https://pypi.python.org/pypi/blobxfer
.. _Docker Hub: https://hub.docker.com/r/alfpark/blobxfer
.. _azure-common: https://pypi.python.org/pypi/azure-common
.. _azure-storage: https://pypi.python.org/pypi/azure-storage
.. _requests: https://pypi.python.org/pypi/requests
.. _cryptography: https://pypi.python.org/pypi/cryptography
.. _azure-servicemanagement-legacy: https://pypi.python.org/pypi/azure-servicemanagement-legacy
Introduction
------------
The blobxfer.py script allows interacting with storage accounts using any of
the following methods: (1) management certificate, (2) shared account key,
(3) SAS key. The script can, in addition to working with single files, mirror
entire directories into and out of containers or file shares from Azure
Storage, respectively. File and block/page level MD5 integrity checking is
supported along with various transfer optimizations, built-in retries,
user-specified timeouts, and client-side encryption.
Program parameters and command-line options can be listed via the ``-h``
switch. Please invoke this first if you are unfamiliar with blobxfer operation
as not all options are explained below. At the minimum, three positional
arguments are required: storage account name, container or share name, and
local resource. Additionally, one of the following authentication switches
must be supplied: ``--subscriptionid`` with ``--managementcert``,
``--storageaccountkey``, or ``--saskey``. Do not combine different
authentication schemes together.
Environment variables ``BLOBXFER_STORAGEACCOUNTKEY``, ``BLOBXFER_SASKEY``,
and ``BLOBXFER_RSAKEYPASSPHRASE`` can take the place of
``--storageaccountkey``, ``--saskey``, and ``--rsakeypassphrase`` respectively
if you do not want to expose credentials on a command line.
It is generally recommended to use SAS keys wherever appropriate; only HTTPS
transport is used in the script. Please note that when using SAS keys that
only container- or fileshare-level SAS keys will allow for entire directory
uploading or container/fileshare downloading. The container/fileshare must
also have been created beforehand if using a service SAS, as
containers/fileshares cannot be created using service SAS keys. Account-level
SAS keys with a signed resource type of ``c`` or container will allow
containers/fileshares to be created with SAS keys.
Example Usage
-------------
The following examples show how to invoke the script with commonly used
options. Note that the authentication parameters are missing from the below
examples. You will need to select a preferred method of authenticating with
Azure and add the authentication switches (or as environment variables) as
noted above.
The script will attempt to perform a smart transfer, by detecting if the local
resource exists. For example:
::
blobxfer mystorageacct container0 mylocalfile.txt
Note: if you downloaded the script directly from github, then you should append
``.py`` to the blobxfer command.
If mylocalfile.txt exists locally, then the script will attempt to upload the
file to container0 on mystorageacct. If the file does not exist, then it will
attempt to download the resource. If the desired behavior is to download the
file from Azure even if the local file exists, one can override the detection
mechanism with ``--download``. ``--upload`` is available to force the transfer
to Azure storage. Note that specifying a particular direction does not force
the actual operation to occur as that depends on other options specified such
as skipping on MD5 matches. Note that you may use the ``--remoteresource`` flag
to rename the local file as the blob name on Azure storage if uploading,
however, ``--remoteresource`` has no effect if uploading a directory of files.
Please refer to the ``--collate`` option as explained below.
If the local resource is a directory that exists, the script will attempt to
mirror (recursively copy) the entire directory to Azure storage while
maintaining subdirectories as virtual directories in Azure storage. You can
disable the recursive copy (i.e., upload only the files in the directory)
using the ``--no-recursive`` flag.
To upload a directory with files only matching a Unix-style shell wildcard
pattern, an example commandline would be:
::
blobxfer mystorageacct container0 mylocaldir --upload --include '**/*.txt'
This would attempt to recursively upload the contents of mylocaldir
to container0 for any file matching the wildcard pattern ``*.txt`` within
all subdirectories. Include patterns can be applied for uploads as well as
downloads. Note that you will need to prevent globbing by your shell such
that wildcard expansion does not take place before script interprets the
argument. If ``--include`` is not specified, all files will be uploaded
or downloaded for the specific context.
To download an entire container from your storage account, an example
commandline would be:
::
blobxfer mystorageacct container0 mylocaldir --remoteresource .
Assuming mylocaldir directory does not exist, the script will attempt to
download all of the contents in container0 because “.” is set with
``--remoteresource`` flag. To download individual blobs, one would specify the
blob name instead of “.” with the ``--remoteresource`` flag. If mylocaldir
directory exists, the script will attempt to upload the directory instead of
downloading it. If you want to force the download direction even if the
directory exists, indicate that with the ``--download`` flag. When downloading
an entire container, the script will attempt to pre-allocate file space and
recreate the sub-directory structure as needed.
To collate files into specified virtual directories or local paths, use
the ``--collate`` flag with the appropriate parameter. For example, the
following commandline:
::
blobxfer mystorageacct container0 myvhds --upload --collate vhds --autovhd
If the directory ``myvhds`` had two vhd files a.vhd and subdir/b.vhd, these
files would be uploaded into ``container0`` under the virtual directory named
``vhds``, and b.vhd would not contain the virtual directory subdir; thus,
flattening the directory structure. The ``--autovhd`` flag would automatically
enable page blob uploads for these files. If you wish to collate all files
into the container directly, you would replace ``--collate vhds`` with
``--collate .``
To strip leading components of a path on upload, use ``--strip-components``
with a number argument which will act similarly to tar's
``--strip-components=NUMBER`` parameter. This parameter is only applied
during an upload.
To encrypt or decrypt files, the option ``--rsapublickey`` and
``--rsaprivatekey`` is available. This option requires a file location for a
PEM encoded RSA public or private key. An optional parameter,
``--rsakeypassphrase`` is available for passphrase protected RSA private keys.
To encrypt and upload, only the RSA public key is required although an RSA
private key may be specified. To download and decrypt blobs which are
encrypted, the RSA private key is required.
::
blobxfer mystorageacct container0 myblobs --upload --rsapublickey mypublickey.pem
The above example commandline would encrypt and upload files contained in
``myblobs`` using an RSA public key named ``mypublickey.pem``. An RSA private
key may be specified instead for uploading (public parts will be used).
::
blobxfer mystorageacct container0 myblobs --remoteresource . --download --rsaprivatekey myprivatekey.pem
The above example commandline would download and decrypt all blobs in the
container ``container0`` using an RSA private key named ``myprivatekey.pem``.
An RSA private key must be specified for downloading and decryption of
encrypted blobs.
Currently only the ``FullBlob`` encryption mode is supported for the
parameter ``--encmode``. The ``FullBlob`` encryption mode either uploads or
downloads Azure Storage .NET/Java compatible client-side encrypted block blobs.
Please read important points in the Encryption Notes below for more
information.
To transfer to an Azure Files share, specify the ``--fileshare`` option and
specify the share name as the second positional argument.
::
blobxfer mystorageacct myshare localfiles --fileshare --upload
The above example would upload all files in the ``localfiles`` directory to
the share named ``myshare``. Encryption/decryption options are compatible with
Azure Files as the destination or source. Please refer to this `MSDN article`_
for features not supported by the Azure File Service.
.. _MSDN article: https://msdn.microsoft.com/en-us/library/azure/dn744326.aspx
Docker Usage
------------
An example execution for uploading the host path ``/example/host/path``
to a storage container named ``container0`` would be:
::
docker run --rm -t -v /example/host/path:/path/in/container alfpark/blobxfer mystorageacct container0 /path/in/container --upload
Note that docker volume mount mappings must be crafted with care to ensure
consistency with directory depth between the host and the container.
Optionally, you can utilize the ``--strip-components`` flag to remove leading
path components as desired.
General Notes
-------------
- If the pyOpenSSL package is present, urllib3/requests may use this package
(as discussed in the Performance Notes below), which may result in
exceptions being thrown that are not normalized by urllib3. This may
result in exceptions that should be retried, but are not. It is recommended
to upgrade your Python where pyOpenSSL is not required for fully validating
peers and such that blobxfer can operate without pyOpenSSL in a secure
fashion. You can also run blobxfer via Docker or in a virtualenv
environment without pyOpenSSL.
- blobxfer does not take any leases on blobs or containers. It is up to
the user to ensure that blobs are not modified while download/uploads
are being performed.
- No validation is performed regarding container and file naming and length
restrictions.
- blobxfer will attempt to download from blob storage as-is. If the source
filename is incompatible with the destination operating system, then
failure may result.
- When using SAS, the SAS key must be a container- or share-level SAS if
performing recursive directory upload or container/file share download.
- If uploading via service-level SAS keys, the container or file share must
already be created in Azure storage prior to upload. Account-level SAS keys
with the signed resource type of ``c`` or container-level permission will
allow conatiner or file share creation.
- For non-SAS requests, timeouts may not be properly honored due to
limitations of the Azure Python SDK.
- By default, files with matching MD5 checksums will be skipped for both
download (if MD5 information is present on the blob) and upload. Specify
``--no-skiponmatch`` to disable this functionality.
- When uploading files as page blobs, the content is page boundary
byte-aligned. The MD5 for the blob is computed using the final aligned
data if the source is not page boundary byte-aligned. This enables these
page blobs or files to be skipped during subsequent download or upload by
default (i.e., ``--no-skiponmatch`` parameter is not specified).
- If ``--delete`` is specified, any remote files found that have no
corresponding local file in directory upload mode will be deleted. Deletion
occurs prior to any transfers, analogous to the delete-before rsync option.
Please note that this parameter will interact with ``--include`` and any
file not included from the include pattern will be deleted.
- ``--include`` has no effect when specifying a single file to upload or
blob to download. When specifying ``--include`` on container download,
the pattern will be applied to the blob name without the container name.
Globbing of wildcards must be disabled such that the script can read
the include pattern without the shell expanding the wildcards, if specified.
- Empty directories are not created locally when downloading from an Azure
file share which has empty directories.
- Empty directories are not deleted if ``--delete`` is specified and no
files remain in the directory on the Azure file share.
Performance Notes
-----------------
- Most likely, you will need to tweak the ``--numworkers`` argument that best
suits your environment. The default is the number of CPUs on the running
machine multiplied by 3 (except when transferring to/from file shares).
Increasing this number (or even using the default) may not provide the
optimal balance between concurrency and your network conditions.
Additionally, this number may not work properly if you are attempting to
run multiple blobxfer sessions in parallel from one machine or IP address.
Futhermore, this number may be defaulted to be set too high if encryption
is enabled and the machine cannot handle processing multiple threads in
parallel.
- Computing file MD5 can be time consuming for large files. If integrity
checking or rsync-like capability is not required, specify
``--no-computefilemd5`` to disable MD5 computation for files.
- File share performance can be "slow" or become a bottleneck, especially for
file shares containing thousands of files as multiple REST calls must be
performed for each file. Currently, a single file share has a limit of up
to 60 MB/s and 1000 8KB IOPS. Please refer to the
`Azure Storage Scalability and Performance Targets`_ for performance targets
and limits regarding Azure Storage Blobs and Files. If scalable high
performance is required, consider using blob storage or multiple file
shares.
- Using SAS keys may provide the best performance as the script bypasses
the Azure Storage Python SDK and uses requests/urllib3 directly with
Azure Storage endpoints. Transfers to/from Azure Files will always use
the Azure Storage Python SDK even with SAS keys.
- As of requests 2.6.0 and Python versions < 2.7.9 (i.e., interpreter found
on default Ubuntu 14.04 installations), if certain packages are installed,
as those found in ``requests[security]`` then the underlying ``urllib3``
package will utilize the ``ndg-httpsclient`` package which will use
`pyOpenSSL`_. This will ensure the peers are `fully validated`_. However,
this incurs a rather larger performance penalty. If you understand the
potential security risks for disabling this behavior due to high performance
requirements, you can either remove ``ndg-httpsclient`` or use the script
in a ``virtualenv`` environment without the ``ndg-httpsclient`` package.
Python versions >= 2.7.9 are not affected by this issue. These warnings can
be suppressed using ``--disable-urllib-warnings``, but is not recommended
unless you understand the security implications.
.. _Azure Storage Scalability and Performance Targets: https://azure.microsoft.com/en-us/documentation/articles/storage-scalability-targets/
.. _pyOpenSSL: https://urllib3.readthedocs.org/en/latest/security.html#pyopenssl
.. _fully validated: https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
Encryption Notes
----------------
- All required information regarding the encryption process is stored on
each blob's ``encryptiondata`` and ``encryptiondata_authentication``
metadata. These metadata entries are used on download to configure the proper
download and parameters for the decryption process as well as to authenticate
the encryption. Encryption metadata set by blobxfer (or the Azure Storage
.NET/Java client library) should not be modified or blobs/files may be
unrecoverable.
- Local files can be encrypted by blobxfer and stored in Azure Files and,
correspondingly, remote files on Azure File shares can be decrypted by
blobxfer as long as the metdata portions remain in-tact.
- Keys for AES256 block cipher are generated on a per-blob/file basis. These
keys are encrypted using RSAES-OAEP.
- MD5 for both the pre-encrypted and encrypted version of the file is stored
in blob/file metadata. Rsync-like synchronization is still supported
transparently with encrypted blobs/files.
- Whole file MD5 checks are skipped if a message authentication code is found
to validate the integrity of the encrypted data.
- Attempting to upload the same file as an encrypted blob with a different RSA
key or under a different encryption mode will not occur if the file content
MD5 is the same. This behavior can be overridden by including the option
``--no-skiponmatch``.
- If one wishes to apply encryption to a blob/file already uploaded to Azure
Storage that has not changed, the upload will not occur since the underlying
file content MD5 has not changed; this behavior can be overriden by
including the option ``--no-skiponmatch``.
- Encryption is only applied to block blobs (or fileshare files). Encrypted
page blobs appear to be of minimal value stored in Azure Storage via
blobxfer. Thus, if uploading VHDs while enabling encryption in the script,
do not enable the option ``--pageblob``. ``--autovhd`` will continue to work
transparently where vhd files will be uploaded as page blobs in unencrypted
form while other files will be uploaded as encrypted block blobs. Note that
using ``--autovhd`` with encryption will force set the max chunk size to
4 MiB for non-encrypted vhd files.
- Downloading encrypted blobs/files may not fully preallocate each file due to
padding. Script failure can result during transfer if there is insufficient
disk space.
- Zero-byte (empty) files are not encrypted.
Change Log
----------
See the `CHANGELOG.md`_ file.
.. _CHANGELOG.md: https://github.com/Azure/blobxfer/blob/master/CHANGELOG.md
----
This project has adopted the
`Microsoft Open Source Code of Conduct <https://opensource.microsoft.com/codeofconduct/>`__.
For more information see the
`Code of Conduct FAQ <https://opensource.microsoft.com/codeofconduct/faq/>`__
or contact `opencode@microsoft.com <mailto:opencode@microsoft.com>`__ with any
additional questions or comments.

Разница между файлами не показана из-за своего большого размера Загрузить разницу

25
blobxfer/__init__.py Normal file
Просмотреть файл

@ -0,0 +1,25 @@
# Copyright (c) Microsoft Corporation
#
# All rights reserved.
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
from .version import __version__ # noqa

213
blobxfer/util.py Normal file
Просмотреть файл

@ -0,0 +1,213 @@
# Copyright (c) Microsoft Corporation
#
# All rights reserved.
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
# compat imports
from __future__ import absolute_import, division, print_function
from builtins import ( # noqa
bytes, dict, int, list, object, range, str, ascii, chr, hex, input,
next, oct, open, pow, round, super, filter, map, zip
)
# stdlib imports
import base64
import copy
import hashlib
import logging
import logging.handlers
import mimetypes
try:
from os import scandir as scandir
except ImportError: # noqa
from scandir import scandir as scandir
import sys
# non-stdlib imports
# local imports
# global defines
_PY2 = sys.version_info.major == 2
_PAGEBLOB_BOUNDARY = 512
def on_python2():
# type: (None) -> bool
"""Execution on python2
:rtype: bool
:return: if on Python2
"""
return _PY2
def setup_logger(logger): # noqa
# type: (logger) -> None
"""Set up logger"""
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)sZ %(levelname)s %(name)s:%(funcName)s:%(lineno)d '
'%(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def is_none_or_empty(obj):
# type: (any) -> bool
"""Determine if object is None or empty
:type any obj: object
:rtype: bool
:return: if object is None or empty
"""
if obj is None or len(obj) == 0:
return True
return False
def is_not_empty(obj):
# type: (any) -> bool
"""Determine if object is not None and is length is > 0
:type any obj: object
:rtype: bool
:return: if object is not None and length is > 0
"""
if obj is not None and len(obj) > 0:
return True
return False
def merge_dict(dict1, dict2):
# type: (dict, dict) -> dict
"""Recursively merge dictionaries: dict2 on to dict1. This differs
from dict.update() in that values that are dicts are recursively merged.
Note that only dict value types are merged, not lists, etc.
:param dict dict1: dictionary to merge to
:param dict dict2: dictionary to merge with
:rtype: dict
:return: merged dictionary
"""
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
raise ValueError('dict1 or dict2 is not a dictionary')
result = copy.deepcopy(dict1)
for k, v in dict2.items():
if k in result and isinstance(result[k], dict):
result[k] = merge_dict(result[k], v)
else:
result[k] = copy.deepcopy(v)
return result
def scantree(path):
# type: (str) -> os.DirEntry
"""Recursively scan a directory tree
:param str path: path to scan
:rtype: DirEntry
:return: DirEntry via generator
"""
for entry in scandir(path):
if entry.is_dir(follow_symlinks=True):
# due to python2 compat, cannot use yield from here
for t in scantree(entry.path):
yield t
else:
yield entry
def get_mime_type(filename):
# type: (str) -> str
"""Guess the type of a file based on its filename
:param str filename: filename to guess the content-type
:rtype: str
:rturn: string of form 'class/type' for MIME content-type header
"""
return (mimetypes.guess_type(filename)[0] or 'application/octet-stream')
def base64_encode_as_string(obj): # noqa
# type: (any) -> str
"""Encode object to base64
:param any obj: object to encode
:rtype: str
:return: base64 encoded string
"""
if _PY2:
return base64.b64encode(obj)
else:
return str(base64.b64encode(obj), 'ascii')
def base64_decode_string(string):
# type: (str) -> str
"""Base64 decode a string
:param str string: string to decode
:rtype: str
:return: decoded string
"""
return base64.b64decode(string)
def compute_md5_for_file_asbase64(filename, pagealign=False, blocksize=65536):
# type: (str, bool, int) -> str
"""Compute MD5 hash for file and encode as Base64
:param str filename: file to compute MD5 for
:param bool pagealign: page align data
:param int blocksize: block size
:rtype: str
:return: MD5 for file encoded as Base64
"""
hasher = hashlib.md5()
with open(filename, 'rb') as filedesc:
while True:
buf = filedesc.read(blocksize)
if not buf:
break
buflen = len(buf)
if pagealign and buflen < blocksize:
aligned = page_align_content_length(buflen)
if aligned != buflen:
buf = buf.ljust(aligned, b'\0')
hasher.update(buf)
return base64_encode_as_string(hasher.digest())
def compute_md5_for_data_asbase64(data):
# type: (obj) -> str
"""Compute MD5 hash for bits and encode as Base64
:param any data: data to compute MD5 for
:rtype: str
:return: MD5 for data
"""
hasher = hashlib.md5()
hasher.update(data)
return base64_encode_as_string(hasher.digest())
def page_align_content_length(length):
# type: (int) -> int
"""Compute page boundary alignment
:param int length: content length
:rtype: int
:return: aligned byte boundary
"""
mod = length % _PAGEBLOB_BOUNDARY
if mod != 0:
return length + (_PAGEBLOB_BOUNDARY - mod)
return length

25
blobxfer/version.py Normal file
Просмотреть файл

@ -0,0 +1,25 @@
# Copyright (c) Microsoft Corporation
#
# All rights reserved.
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
__version__ = '1.0.0a1'

Просмотреть файл

@ -1,41 +1,76 @@
from codecs import open
import os
import re import re
try: try:
from setuptools import setup from setuptools import setup
except ImportError: except ImportError:
from distutils.core import setup from distutils.core import setup
import sys
with open('blobxfer.py', 'r') as fd: if sys.argv[-1] == 'publish':
os.system('rm -rf blobxfer.egg-info/ build dist __pycache__/')
os.system('python setup.py sdist bdist_wheel')
os.unlink('README.rst')
sys.exit()
elif sys.argv[-1] == 'upload':
os.system('twine upload dist/*')
sys.exit()
elif sys.argv[-1] == 'sdist' or sys.argv[-1] == 'bdist_wheel':
import pypandoc
long_description = pypandoc.convert('README.md', 'rst')
else:
long_description = ''
with open('blobxfer/version.py', 'r', 'utf-8') as fd:
version = re.search( version = re.search(
r'^_SCRIPT_VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
fd.read(), re.MULTILINE).group(1) fd.read(), re.MULTILINE).group(1)
with open('README.rst') as readme: if not version:
long_description = ''.join(readme).strip() raise RuntimeError('Cannot find version')
packages = [
'blobxfer',
'blobxfer.blob',
'blobxfer.blob.block',
'blobxfer_cli',
]
install_requires = [
'azure-common==1.1.4',
'azure-storage==0.33.0',
'click==6.6',
'cryptography>=1.7.1',
'future==0.16.0',
'ruamel.yaml==0.13.11',
]
if sys.version_info < (3, 5):
install_requires.append('pathlib2')
install_requires.append('scandir')
setup( setup(
name='blobxfer', name='blobxfer',
version=version, version=version,
author='Microsoft Corporation, Azure Batch and HPC Team', author='Microsoft Corporation, Azure Batch and HPC Team',
author_email='', author_email='',
description='Azure storage transfer tool with AzCopy-like features', description=(
'Azure storage transfer tool and library with AzCopy-like features'),
long_description=long_description, long_description=long_description,
platforms='any', platforms='any',
url='https://github.com/Azure/blobxfer', url='https://github.com/Azure/blobxfer',
license='MIT', license='MIT',
py_modules=['blobxfer'], packages=packages,
package_data={'blobxfer': ['LICENSE']},
package_dir={'blobxfer': 'blobxfer', 'blobxfer_cli': 'cli'},
entry_points={ entry_points={
'console_scripts': 'blobxfer=blobxfer:main', 'console_scripts': 'blobxfer=blobxfer_cli.cli:cli',
}, },
install_requires=[ zip_safe=False,
'azure-common==1.1.4', install_requires=install_requires,
'azure-storage==0.33.0',
'azure-servicemanagement-legacy==0.20.5',
'cryptography>=1.6',
'requests==2.12.3'
],
tests_require=['pytest'], tests_require=['pytest'],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 3 - Alpha',
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
@ -47,7 +82,8 @@ setup(
'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Utilities', 'Topic :: Utilities',
], ],
keywords='azcopy azure storage blob files transfer copy smb', keywords='azcopy azure storage blob files transfer copy smb cifs',
) )

Разница между файлами не показана из-за своего большого размера Загрузить разницу

5
test_requirements.txt Normal file
Просмотреть файл

@ -0,0 +1,5 @@
flake8>=3.2.1
mock>=2.0.0
pypandoc>=1.3.3
pytest>=3.0.5
pytest-cov>=2.4.0

133
tests/test_blobxfer_util.py Normal file
Просмотреть файл

@ -0,0 +1,133 @@
# coding=utf-8
"""Tests for util"""
# stdlib imports
import sys
import uuid
# non-stdlib imports
import pytest
# module under test
import blobxfer.util
def test_on_python2():
py2 = sys.version_info.major == 2
assert py2 == blobxfer.util.on_python2()
def test_is_none_or_empty():
a = None
assert blobxfer.util.is_none_or_empty(a)
a = []
assert blobxfer.util.is_none_or_empty(a)
a = {}
assert blobxfer.util.is_none_or_empty(a)
a = ''
assert blobxfer.util.is_none_or_empty(a)
a = 'asdf'
assert not blobxfer.util.is_none_or_empty(a)
a = ['asdf']
assert not blobxfer.util.is_none_or_empty(a)
a = {'asdf': 0}
assert not blobxfer.util.is_none_or_empty(a)
a = [None]
assert not blobxfer.util.is_none_or_empty(a)
def test_is_not_empty():
a = None
assert not blobxfer.util.is_not_empty(a)
a = []
assert not blobxfer.util.is_not_empty(a)
a = {}
assert not blobxfer.util.is_not_empty(a)
a = ''
assert not blobxfer.util.is_not_empty(a)
a = 'asdf'
assert blobxfer.util.is_not_empty(a)
a = ['asdf']
assert blobxfer.util.is_not_empty(a)
a = {'asdf': 0}
assert blobxfer.util.is_not_empty(a)
a = [None]
assert blobxfer.util.is_not_empty(a)
def test_merge_dict():
with pytest.raises(ValueError):
blobxfer.util.merge_dict(1, 2)
a = {'a_only': 42, 'a_and_b': 43,
'a_only_dict': {'a': 44}, 'a_and_b_dict': {'a_o': 45, 'a_a_b': 46}}
b = {'b_only': 45, 'a_and_b': 46,
'b_only_dict': {'a': 47}, 'a_and_b_dict': {'b_o': 48, 'a_a_b': 49}}
c = blobxfer.util.merge_dict(a, b)
assert c['a_only'] == 42
assert c['b_only'] == 45
assert c['a_and_b_dict']['a_o'] == 45
assert c['a_and_b_dict']['b_o'] == 48
assert c['a_and_b_dict']['a_a_b'] == 49
assert c['b_only_dict']['a'] == 47
assert c['a_and_b'] == 46
assert a['a_only'] == 42
assert a['a_and_b'] == 43
assert b['b_only'] == 45
assert b['a_and_b'] == 46
def test_scantree(tmpdir):
tmpdir.mkdir('abc')
abcpath = tmpdir.join('abc')
abcpath.join('hello.txt').write('hello')
abcpath.mkdir('def')
defpath = abcpath.join('def')
defpath.join('world.txt').write('world')
found = set()
for de in blobxfer.util.scantree(str(tmpdir.dirpath())):
if de.name != '.lock':
found.add(de.name)
assert 'hello.txt' in found
assert 'world.txt' in found
assert len(found) == 2
def test_get_mime_type():
a = 'b.txt'
mt = blobxfer.util.get_mime_type(a)
assert mt == 'text/plain'
a = 'c.probably_cant_determine_this'
mt = blobxfer.util.get_mime_type(a)
assert mt == 'application/octet-stream'
def test_base64_encode_as_string():
a = b'abc'
enc = blobxfer.util.base64_encode_as_string(a)
assert type(enc) != bytes
dec = blobxfer.util.base64_decode_string(enc)
assert a == dec
def test_compute_md5(tmpdir):
lpath = str(tmpdir.join('test.tmp'))
testdata = str(uuid.uuid4())
with open(lpath, 'wt') as f:
f.write(testdata)
md5_file = blobxfer.util.compute_md5_for_file_asbase64(lpath)
md5_data = blobxfer.util.compute_md5_for_data_asbase64(
testdata.encode('utf8'))
assert md5_file == md5_data
md5_file_page = blobxfer.util.compute_md5_for_file_asbase64(lpath, True)
assert md5_file != md5_file_page
# test non-existent file
with pytest.raises(IOError):
blobxfer.util.compute_md5_for_file_asbase64(testdata)
def test_page_align_content_length():
assert 0 == blobxfer.util.page_align_content_length(0)
assert 512 == blobxfer.util.page_align_content_length(511)
assert 512 == blobxfer.util.page_align_content_length(512)
assert 1024 == blobxfer.util.page_align_content_length(513)

18
tox.ini Normal file
Просмотреть файл

@ -0,0 +1,18 @@
[tox]
envlist = py35
[testenv]
deps = -rtest_requirements.txt
commands =
#flake8 {envsitepackagesdir}/blobxfer_cli/
#flake8 {envsitepackagesdir}/blobxfer/
py.test \
-x -l -s \
--ignore venv/ \
--cov-config .coveragerc \
--cov-report term-missing \
--cov {envsitepackagesdir}/blobxfer
[flake8]
max-line-length = 79
select = F,E,W