mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-13 15:28:27 +01:00
228 lines
7.0 KiB
Python
228 lines
7.0 KiB
Python
import os
|
|
import re
|
|
import time
|
|
|
|
try:
|
|
from azure.storage.blob import BlockBlobService
|
|
except ImportError:
|
|
BlockBlobService = None
|
|
|
|
try:
|
|
from urlparse import urlparse
|
|
except ImportError:
|
|
from urllib.parse import urlparse
|
|
|
|
from dvc.logger import Logger
|
|
from dvc.progress import progress
|
|
from dvc.config import Config
|
|
from dvc.remote.base import RemoteBase
|
|
|
|
|
|
class Callback(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __call__(self, current, total):
|
|
progress.update_target(self.name, current, total)
|
|
|
|
|
|
class RemoteAzure(RemoteBase):
|
|
scheme = 'azure'
|
|
REGEX = (r'^azure://'
|
|
r'(ContainerName=(?P<container_name>[^;]+);?)?'
|
|
r'(?P<connection_string>.+)?$')
|
|
REQUIRES = {'azure-storage-blob': BlockBlobService}
|
|
PARAM_ETAG = 'etag'
|
|
COPY_POLL_SECONDS = 5
|
|
|
|
def __init__(self, project, config):
|
|
super().__init__(project, config)
|
|
self.project = project
|
|
|
|
url = config.get(Config.SECTION_REMOTE_URL)
|
|
match = re.match(self.REGEX, url)
|
|
|
|
self.bucket = (
|
|
match.group('container_name')
|
|
or os.getenv('AZURE_STORAGE_CONTAINER_NAME'))
|
|
|
|
self.connection_string = (
|
|
match.group('connection_string')
|
|
or os.getenv('AZURE_STORAGE_CONNECTION_STRING'))
|
|
|
|
if not self.bucket:
|
|
raise ValueError('Azure Storage container name missing')
|
|
|
|
if not self.connection_string:
|
|
raise ValueError('Azure Storage connection string missing')
|
|
|
|
self.__blob_service = None
|
|
|
|
@property
|
|
def blob_service(self):
|
|
if self.__blob_service is None:
|
|
self.__blob_service = BlockBlobService(
|
|
connection_string=self.connection_string)
|
|
self.__blob_service.create_container(self.bucket)
|
|
return self.__blob_service
|
|
|
|
def _get_etag(self, bucket, key):
|
|
try:
|
|
blob = self.blob_service.get_blob_properties(bucket, key)
|
|
return blob.properties.etag
|
|
except Exception:
|
|
return None
|
|
|
|
def save_info(self, path_info):
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
return {self.PARAM_ETAG: self._get_etag(
|
|
path_info['bucket'], path_info['key'])}
|
|
|
|
def save(self, path_info):
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
etag = self._get_etag(path_info['bucket'], path_info['key'])
|
|
dest_key = '{}/{}'.format(etag[0:2], etag[2:])
|
|
|
|
self._copy_blob(to_bucket=self.bucket,
|
|
to_key=dest_key,
|
|
from_bucket=path_info['bucket'],
|
|
from_key=path_info['key'])
|
|
|
|
return {self.PARAM_ETAG: etag}
|
|
|
|
def _copy_blob(self, to_bucket, to_key, from_bucket, from_key):
|
|
source = self.blob_service.make_blob_url(from_bucket, from_key)
|
|
|
|
copy = self.blob_service.copy_blob(to_bucket, to_key, source)
|
|
|
|
if self.COPY_POLL_SECONDS <= 0:
|
|
return
|
|
|
|
while copy.status != 'success':
|
|
time.sleep(self.COPY_POLL_SECONDS)
|
|
copy = self.blob_service.get_blob_properties(
|
|
to_bucket, to_key).properties.copy
|
|
|
|
def checkout(self, path_info, checksum_info):
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
etag = checksum_info.get(self.PARAM_ETAG, None)
|
|
if not etag:
|
|
return
|
|
|
|
self._copy_blob(to_bucket=path_info['bucket'],
|
|
to_key=path_info['key'],
|
|
from_bucket=self.bucket,
|
|
from_key='{}/{}'.format(etag[0:2], etag[2:]))
|
|
|
|
def remove(self, path_info):
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
Logger.debug('Removing azure://{}/{}'.format(path_info['bucket'],
|
|
path_info['key']))
|
|
|
|
self.blob_service.delete_blob(path_info['bucket'], path_info['key'])
|
|
|
|
def md5s_to_path_infos(self, md5s):
|
|
return [{
|
|
'scheme': self.scheme,
|
|
'bucket': self.bucket,
|
|
'key': '{}/{}'.format(md5[0:2], md5[2:])
|
|
} for md5 in md5s]
|
|
|
|
def exists(self, path_infos):
|
|
# TODO: check whether per-key exists() is faster than listing all
|
|
# objects altogether.
|
|
ret = []
|
|
|
|
for path_info in path_infos:
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
bucket = path_info['bucket']
|
|
key = path_info['key']
|
|
|
|
exists = self.blob_service.exists(bucket, key)
|
|
ret.append(exists)
|
|
|
|
return ret
|
|
|
|
def upload(self, paths, path_infos, names=None):
|
|
names = self._verify_path_args(path_infos, paths, names)
|
|
|
|
for path, path_info, name in zip(paths, path_infos, names):
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
bucket = path_info['bucket']
|
|
key = path_info['key']
|
|
|
|
Logger.debug("Uploading '{}' to '{}/{}'".format(
|
|
path, bucket, key))
|
|
|
|
if not name:
|
|
name = os.path.basename(path)
|
|
|
|
cb = Callback(name)
|
|
|
|
try:
|
|
self.blob_service.create_blob_from_path(
|
|
bucket, key, path, progress_callback=cb)
|
|
except Exception as ex:
|
|
Logger.error("Failed to upload '{}'".format(path), ex)
|
|
else:
|
|
progress.finish_target(name)
|
|
|
|
def download(self, path_infos, fnames, no_progress_bar=False, names=None):
|
|
names = self._verify_path_args(path_infos, fnames, names)
|
|
|
|
for fname, path_info, name in zip(fnames, path_infos, names):
|
|
if path_info['scheme'] != self.scheme:
|
|
raise NotImplementedError
|
|
|
|
bucket = path_info['bucket']
|
|
key = path_info['key']
|
|
|
|
Logger.debug("Downloading '{}/{}' to '{}'".format(
|
|
bucket, key, fname))
|
|
|
|
tmp_file = self.tmp_file(fname)
|
|
if not name:
|
|
name = os.path.basename(fname)
|
|
|
|
cb = None if no_progress_bar else Callback(name)
|
|
|
|
self._makedirs(fname)
|
|
|
|
try:
|
|
self.blob_service.get_blob_to_path(
|
|
bucket, key, tmp_file, progress_callback=cb)
|
|
except Exception as exc:
|
|
Logger.error("Failed to download '{}/{}'".format(
|
|
bucket, key), exc)
|
|
else:
|
|
os.rename(tmp_file, fname)
|
|
|
|
if not no_progress_bar:
|
|
progress.finish_target(name)
|
|
|
|
def gc(self, checksum_infos):
|
|
used_etags = [info[self.PARAM_ETAG] for info in checksum_infos]
|
|
|
|
all_blobs = self.blob_service.list_blobs(self.bucket)
|
|
|
|
for blob in all_blobs:
|
|
etag = blob.properties.etag
|
|
if etag in used_etags:
|
|
continue
|
|
path_info = {'scheme': self.scheme,
|
|
'key': blob.name,
|
|
'bucket': self.bucket}
|
|
self.remove(path_info)
|