Spaces:
Running
Running
File size: 11,408 Bytes
375a1cf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
"""
This module contains SMBFileSystem class responsible for handling access to
Windows Samba network shares by using package smbprotocol
"""
import datetime
import uuid
from stat import S_ISDIR, S_ISLNK
import smbclient
from .. import AbstractFileSystem
from ..utils import infer_storage_options
# ! pylint: disable=bad-continuation
class SMBFileSystem(AbstractFileSystem):
"""Allow reading and writing to Windows and Samba network shares.
When using `fsspec.open()` for getting a file-like object the URI
should be specified as this format:
``smb://workgroup;user:password@server:port/share/folder/file.csv``.
Example::
>>> import fsspec
>>> with fsspec.open(
... 'smb://myuser:[email protected]/' 'share/folder/file.csv'
... ) as smbfile:
... df = pd.read_csv(smbfile, sep='|', header=None)
Note that you need to pass in a valid hostname or IP address for the host
component of the URL. Do not use the Windows/NetBIOS machine name for the
host component.
The first component of the path in the URL points to the name of the shared
folder. Subsequent path components will point to the directory/folder/file.
The URL components ``workgroup`` , ``user``, ``password`` and ``port`` may be
optional.
.. note::
For working this source require `smbprotocol`_ to be installed, e.g.::
$ pip install smbprotocol
# or
# pip install smbprotocol[kerberos]
.. _smbprotocol: https://github.com/jborean93/smbprotocol#requirements
Note: if using this with the ``open`` or ``open_files``, with full URLs,
there is no way to tell if a path is relative, so all paths are assumed
to be absolute.
"""
protocol = "smb"
# pylint: disable=too-many-arguments
def __init__(
self,
host,
port=None,
username=None,
password=None,
timeout=60,
encrypt=None,
share_access=None,
register_session_retries=5,
auto_mkdir=False,
**kwargs,
):
"""
You can use _get_kwargs_from_urls to get some kwargs from
a reasonable SMB url.
Authentication will be anonymous or integrated if username/password are not
given.
Parameters
----------
host: str
The remote server name/ip to connect to
port: int or None
Port to connect with. Usually 445, sometimes 139.
username: str or None
Username to connect with. Required if Kerberos auth is not being used.
password: str or None
User's password on the server, if using username
timeout: int
Connection timeout in seconds
encrypt: bool
Whether to force encryption or not, once this has been set to True
the session cannot be changed back to False.
share_access: str or None
Specifies the default access applied to file open operations
performed with this file system object.
This affects whether other processes can concurrently open a handle
to the same file.
- None (the default): exclusively locks the file until closed.
- 'r': Allow other handles to be opened with read access.
- 'w': Allow other handles to be opened with write access.
- 'd': Allow other handles to be opened with delete access.
auto_mkdir: bool
Whether, when opening a file, the directory containing it should
be created (if it doesn't already exist). This is assumed by pyarrow
and zarr-python code.
"""
super().__init__(**kwargs)
self.host = host
self.port = port
self.username = username
self.password = password
self.timeout = timeout
self.encrypt = encrypt
self.temppath = kwargs.pop("temppath", "")
self.share_access = share_access
self.register_session_retries = register_session_retries
self.auto_mkdir = auto_mkdir
self._connect()
@property
def _port(self):
return 445 if self.port is None else self.port
def _connect(self):
import time
for _ in range(self.register_session_retries):
try:
smbclient.register_session(
self.host,
username=self.username,
password=self.password,
port=self._port,
encrypt=self.encrypt,
connection_timeout=self.timeout,
)
break
except Exception:
time.sleep(0.1)
@classmethod
def _strip_protocol(cls, path):
return infer_storage_options(path)["path"]
@staticmethod
def _get_kwargs_from_urls(path):
# smb://workgroup;user:password@host:port/share/folder/file.csv
out = infer_storage_options(path)
out.pop("path", None)
out.pop("protocol", None)
return out
def mkdir(self, path, create_parents=True, **kwargs):
wpath = _as_unc_path(self.host, path)
if create_parents:
smbclient.makedirs(wpath, exist_ok=False, port=self._port, **kwargs)
else:
smbclient.mkdir(wpath, port=self._port, **kwargs)
def makedirs(self, path, exist_ok=False):
if _share_has_path(path):
wpath = _as_unc_path(self.host, path)
smbclient.makedirs(wpath, exist_ok=exist_ok, port=self._port)
def rmdir(self, path):
if _share_has_path(path):
wpath = _as_unc_path(self.host, path)
smbclient.rmdir(wpath, port=self._port)
def info(self, path, **kwargs):
wpath = _as_unc_path(self.host, path)
stats = smbclient.stat(wpath, port=self._port, **kwargs)
if S_ISDIR(stats.st_mode):
stype = "directory"
elif S_ISLNK(stats.st_mode):
stype = "link"
else:
stype = "file"
res = {
"name": path + "/" if stype == "directory" else path,
"size": stats.st_size,
"type": stype,
"uid": stats.st_uid,
"gid": stats.st_gid,
"time": stats.st_atime,
"mtime": stats.st_mtime,
}
return res
def created(self, path):
"""Return the created timestamp of a file as a datetime.datetime"""
wpath = _as_unc_path(self.host, path)
stats = smbclient.stat(wpath, port=self._port)
return datetime.datetime.fromtimestamp(stats.st_ctime, tz=datetime.timezone.utc)
def modified(self, path):
"""Return the modified timestamp of a file as a datetime.datetime"""
wpath = _as_unc_path(self.host, path)
stats = smbclient.stat(wpath, port=self._port)
return datetime.datetime.fromtimestamp(stats.st_mtime, tz=datetime.timezone.utc)
def ls(self, path, detail=True, **kwargs):
unc = _as_unc_path(self.host, path)
listed = smbclient.listdir(unc, port=self._port, **kwargs)
dirs = ["/".join([path.rstrip("/"), p]) for p in listed]
if detail:
dirs = [self.info(d) for d in dirs]
return dirs
# pylint: disable=too-many-arguments
def _open(
self,
path,
mode="rb",
block_size=-1,
autocommit=True,
cache_options=None,
**kwargs,
):
"""
block_size: int or None
If 0, no buffering, 1, line buffering, >1, buffer that many bytes
Notes
-----
By specifying 'share_access' in 'kwargs' it is possible to override the
default shared access setting applied in the constructor of this object.
"""
if self.auto_mkdir and "w" in mode:
self.makedirs(self._parent(path), exist_ok=True)
bls = block_size if block_size is not None and block_size >= 0 else -1
wpath = _as_unc_path(self.host, path)
share_access = kwargs.pop("share_access", self.share_access)
if "w" in mode and autocommit is False:
temp = _as_temp_path(self.host, path, self.temppath)
return SMBFileOpener(
wpath, temp, mode, port=self._port, block_size=bls, **kwargs
)
return smbclient.open_file(
wpath,
mode,
buffering=bls,
share_access=share_access,
port=self._port,
**kwargs,
)
def copy(self, path1, path2, **kwargs):
"""Copy within two locations in the same filesystem"""
wpath1 = _as_unc_path(self.host, path1)
wpath2 = _as_unc_path(self.host, path2)
if self.auto_mkdir:
self.makedirs(self._parent(path2), exist_ok=True)
smbclient.copyfile(wpath1, wpath2, port=self._port, **kwargs)
def _rm(self, path):
if _share_has_path(path):
wpath = _as_unc_path(self.host, path)
stats = smbclient.stat(wpath, port=self._port)
if S_ISDIR(stats.st_mode):
smbclient.rmdir(wpath, port=self._port)
else:
smbclient.remove(wpath, port=self._port)
def mv(self, path1, path2, recursive=None, maxdepth=None, **kwargs):
wpath1 = _as_unc_path(self.host, path1)
wpath2 = _as_unc_path(self.host, path2)
smbclient.rename(wpath1, wpath2, port=self._port, **kwargs)
def _as_unc_path(host, path):
rpath = path.replace("/", "\\")
unc = f"\\\\{host}{rpath}"
return unc
def _as_temp_path(host, path, temppath):
share = path.split("/")[1]
temp_file = f"/{share}{temppath}/{uuid.uuid4()}"
unc = _as_unc_path(host, temp_file)
return unc
def _share_has_path(path):
parts = path.count("/")
if path.endswith("/"):
return parts > 2
return parts > 1
class SMBFileOpener:
"""writes to remote temporary file, move on commit"""
def __init__(self, path, temp, mode, port=445, block_size=-1, **kwargs):
self.path = path
self.temp = temp
self.mode = mode
self.block_size = block_size
self.kwargs = kwargs
self.smbfile = None
self._incontext = False
self.port = port
self._open()
def _open(self):
if self.smbfile is None or self.smbfile.closed:
self.smbfile = smbclient.open_file(
self.temp,
self.mode,
port=self.port,
buffering=self.block_size,
**self.kwargs,
)
def commit(self):
"""Move temp file to definitive on success."""
# TODO: use transaction support in SMB protocol
smbclient.replace(self.temp, self.path, port=self.port)
def discard(self):
"""Remove the temp file on failure."""
smbclient.remove(self.temp, port=self.port)
def __fspath__(self):
return self.path
def __iter__(self):
return self.smbfile.__iter__()
def __getattr__(self, item):
return getattr(self.smbfile, item)
def __enter__(self):
self._incontext = True
return self.smbfile.__enter__()
def __exit__(self, exc_type, exc_value, traceback):
self._incontext = False
self.smbfile.__exit__(exc_type, exc_value, traceback)
|