mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-10 07:18:27 +01:00
When executing `dvc import-url remote://some-webdav/data`, the name of
the WebDAV dependency in the created stage for data used to be `None`.
This resulted in the following error:
```
Traceback (most recent call last):
File "/home/schubert/projects/dvc/dvc/main.py", line 76, in main
ret = cmd.run()
File "/home/schubert/projects/dvc/dvc/command/imp_url.py", line 14, in run
self.repo.imp_url(
File "/home/schubert/projects/dvc/dvc/repo/__init__.py", line 51, in wrapper
return f(repo, *args, **kwargs)
File "/home/schubert/projects/dvc/dvc/repo/scm_context.py", line 4, in run
result = method(repo, *args, **kw)
File "/home/schubert/projects/dvc/dvc/repo/imp_url.py", line 54, in imp_url
stage.run()
File "/home/schubert/miniconda3/tmp/envs/some-project/lib/python3.8/site-packages/funcy/decorators.py", line 39, in wrapper
return deco(call, *dargs, **dkwargs)
File "/home/schubert/projects/dvc/dvc/stage/decorators.py", line 36, in rwlocked
return call()
File "/home/schubert/miniconda3/tmp/envs/some-project/lib/python3.8/site-packages/funcy/decorators.py", line 60, in __call__
return self._func(*self._args, **self._kwargs)
File "/home/schubert/projects/dvc/dvc/stage/__init__.py", line 450, in run
self.save()
File "/home/schubert/projects/dvc/dvc/stage/__init__.py", line 395, in save
self.md5 = self.compute_md5()
File "/home/schubert/projects/dvc/dvc/stage/__init__.py", line 388, in compute_md5
m = compute_md5(self)
File "/home/schubert/projects/dvc/dvc/stage/utils.py", line 167, in compute_md5
return dict_md5(
File "/home/schubert/projects/dvc/dvc/utils/__init__.py", line 118, in dict_md5
return dict_hash(d, "md5", **kwargs)
File "/home/schubert/projects/dvc/dvc/utils/__init__.py", line 113, in dict_hash
byts = json.dumps(filtered, sort_keys=True).encode("utf-8")
File "/home/schubert/miniconda3/tmp/envs/some-project/lib/python3.8/json/__init__.py", line 234, in dumps
return cls(
File "/home/schubert/miniconda3/tmp/envs/some-project/lib/python3.8/json/encoder.py", line 199, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/home/schubert/miniconda3/tmp/envs/some-project/lib/python3.8/json/encoder.py", line 257, in iterencode
return _iterencode(o, 0)
TypeError: '<' not supported between instances of 'str' and 'NoneType'
```
249 lines
8.0 KiB
Python
249 lines
8.0 KiB
Python
import logging
|
|
import os
|
|
import threading
|
|
from collections import deque
|
|
|
|
from funcy import cached_property, wrap_prop
|
|
|
|
from dvc.config import ConfigError
|
|
from dvc.exceptions import DvcException
|
|
from dvc.hash_info import HashInfo
|
|
from dvc.path_info import HTTPURLInfo, WebDAVURLInfo
|
|
from dvc.progress import Tqdm
|
|
from dvc.scheme import Schemes
|
|
|
|
from .base import BaseTree
|
|
from .http import ask_password
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WebDAVConnectionError(DvcException):
|
|
def __init__(self, host):
|
|
super().__init__(f"Unable to connect to WebDAV {host}.")
|
|
|
|
|
|
class WebDAVTree(BaseTree): # pylint:disable=abstract-method
|
|
# Use webdav scheme
|
|
scheme = Schemes.WEBDAV
|
|
|
|
# URLInfo for Webdav ~ replaces webdav -> http
|
|
PATH_CLS = WebDAVURLInfo
|
|
|
|
# Traversable as walk_files is implemented
|
|
CAN_TRAVERSE = True
|
|
|
|
# Length of walk_files prefix
|
|
TRAVERSE_PREFIX_LEN = 2
|
|
|
|
# Implementation based on webdav3.client
|
|
REQUIRES = {"webdavclient3": "webdav3.client"}
|
|
|
|
# Chunk size for buffered upload/download with progress bar
|
|
CHUNK_SIZE = 2 ** 16
|
|
|
|
PARAM_CHECKSUM = "etag"
|
|
|
|
# Constructor
|
|
def __init__(self, repo, config):
|
|
# Call BaseTree constructor
|
|
super().__init__(repo, config)
|
|
|
|
# Get username from configuration
|
|
self.user = config.get("user", None)
|
|
|
|
# Get password from configuration (might be None ~ not set)
|
|
self.password = config.get("password", None)
|
|
|
|
# Whether to ask for password if it is not set
|
|
self.ask_password = config.get("ask_password", False)
|
|
|
|
# Use token for webdav auth
|
|
self.token = config.get("token", None)
|
|
|
|
# Path to certificate
|
|
self.cert_path = config.get("cert_path", None)
|
|
|
|
# Path to private key
|
|
self.key_path = config.get("key_path", None)
|
|
|
|
# Connection timeout
|
|
self.timeout = config.get("timeout", 30)
|
|
|
|
# Get URL from configuration
|
|
self.url = config.get("url", None)
|
|
|
|
# If URL in config parse path_info
|
|
if self.url:
|
|
self.path_info = self.PATH_CLS(self.url)
|
|
|
|
# If username not specified try to use from URL
|
|
if self.user is None and self.path_info.user is not None:
|
|
self.user = self.path_info.user
|
|
|
|
# If username specified add to path_info
|
|
if self.user is not None:
|
|
self.path_info.user = self.user
|
|
else:
|
|
self.path_info = None
|
|
|
|
# Webdav client
|
|
@wrap_prop(threading.Lock())
|
|
@cached_property
|
|
def _client(self):
|
|
from webdav3.client import Client
|
|
|
|
# Construct hostname from path_info by stripping path
|
|
http_info = HTTPURLInfo(self.path_info.url)
|
|
hostname = http_info.replace(path="").url
|
|
|
|
# Set password or ask for it
|
|
if self.ask_password and self.password is None and self.token is None:
|
|
host, user = self.path_info.host, self.path_info.user
|
|
self.password = ask_password(host, user)
|
|
|
|
# Setup webdav client options dictionary
|
|
options = {
|
|
"webdav_hostname": hostname,
|
|
"webdav_login": self.user,
|
|
"webdav_password": self.password,
|
|
"webdav_token": self.token,
|
|
"webdav_cert_path": self.cert_path,
|
|
"webdav_key_path": self.key_path,
|
|
"webdav_timeout": self.timeout,
|
|
"webdav_chunk_size": self.CHUNK_SIZE,
|
|
}
|
|
|
|
client = Client(options)
|
|
|
|
# Check whether client options are valid
|
|
if not client.valid():
|
|
raise ConfigError(
|
|
f"Configuration for WebDAV {hostname} is invalid."
|
|
)
|
|
|
|
# Check whether connection is valid (root should always exist)
|
|
if not client.check(self.path_info.path):
|
|
raise WebDAVConnectionError(hostname)
|
|
|
|
return client
|
|
|
|
# Checks whether file/directory exists at remote
|
|
def exists(self, path_info, use_dvcignore=True):
|
|
# Use webdav check to test for file existence
|
|
return self._client.check(path_info.path)
|
|
|
|
# Gets file hash 'etag'
|
|
def get_file_hash(self, path_info):
|
|
# Use webdav client info method to get etag
|
|
etag = self._client.info(path_info.path)["etag"].strip('"')
|
|
|
|
# From HTTPTree
|
|
if not etag:
|
|
raise DvcException(
|
|
"could not find an ETag or "
|
|
"Content-MD5 header for '{url}'".format(url=path_info.url)
|
|
)
|
|
|
|
return HashInfo(self.PARAM_CHECKSUM, etag)
|
|
|
|
# Checks whether path points to directory
|
|
def isdir(self, path_info):
|
|
# Use webdav is_dir to test whether path points to a directory
|
|
return self._client.is_dir(path_info.path)
|
|
|
|
# Yields path info to all files
|
|
def walk_files(self, path_info, **kwargs):
|
|
# Check whether directory exists
|
|
if not self.exists(path_info):
|
|
return
|
|
|
|
# Collect directories
|
|
dirs = deque([path_info.path])
|
|
|
|
# Iterate all directories found so far
|
|
while dirs:
|
|
# Iterate directory content
|
|
for entry in self._client.list(dirs.pop(), get_info=True):
|
|
# Construct path_info to entry
|
|
info = path_info.replace(path=entry["path"])
|
|
|
|
# Check whether entry is a directory
|
|
if entry["isdir"]:
|
|
# Append new found directory to directory list
|
|
dirs.append(info.path)
|
|
else:
|
|
# Yield path info to non directory
|
|
yield info
|
|
|
|
# Removes file/directory
|
|
def remove(self, path_info):
|
|
# Use webdav client clean (DELETE) method to remove file/directory
|
|
self._client.clean(path_info.path)
|
|
|
|
# Creates directories
|
|
def makedirs(self, path_info):
|
|
# Terminate recursion
|
|
if path_info.path == self.path_info.path or self.exists(path_info):
|
|
return
|
|
|
|
# Recursively descent to root
|
|
self.makedirs(path_info.parent)
|
|
|
|
# Construct directory at current recursion depth
|
|
self._client.mkdir(path_info.path)
|
|
|
|
# Moves file/directory at remote
|
|
def move(self, from_info, to_info, mode=None):
|
|
# Webdav client move
|
|
self._client.move(from_info.path, to_info.path)
|
|
|
|
# Downloads file from remote to file
|
|
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
|
|
# Progress from HTTPTree
|
|
with open(to_file, "wb") as fd:
|
|
with Tqdm.wrapattr(
|
|
fd,
|
|
"write",
|
|
total=None if no_progress_bar else self._file_size(from_info),
|
|
leave=False,
|
|
desc=from_info.url if name is None else name,
|
|
disable=no_progress_bar,
|
|
) as fd_wrapped:
|
|
# Download from WebDAV via buffer
|
|
self._client.download_from(
|
|
buff=fd_wrapped, remote_path=from_info.path
|
|
)
|
|
|
|
# Uploads file to remote
|
|
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
|
|
# First try to create parent directories
|
|
self.makedirs(to_info.parent)
|
|
|
|
# Progress from HTTPTree
|
|
def chunks():
|
|
with open(from_file, "rb") as fd:
|
|
with Tqdm.wrapattr(
|
|
fd,
|
|
"read",
|
|
total=None
|
|
if no_progress_bar
|
|
else os.path.getsize(from_file),
|
|
leave=False,
|
|
desc=to_info.url if name is None else name,
|
|
disable=no_progress_bar,
|
|
) as fd_wrapped:
|
|
while True:
|
|
chunk = fd_wrapped.read(self.CHUNK_SIZE)
|
|
if not chunk:
|
|
break
|
|
yield chunk
|
|
|
|
# Upload to WebDAV via buffer
|
|
self._client.upload_to(buff=chunks(), remote_path=to_info.path)
|
|
|
|
# Queries size of file at remote
|
|
def _file_size(self, path_info):
|
|
# Get file size from info dictionary and convert to int (from str)
|
|
return int(self._client.info(path_info.path)["size"])
|