Skip to content

Commit a29c1a1

Browse files
committed
introduce webdav class
1 parent ae7aa2d commit a29c1a1

File tree

5 files changed

+694
-174
lines changed

5 files changed

+694
-174
lines changed

pym/bob/archive.py

Lines changed: 55 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,25 @@
1919
concurrent uploads the artifact must appear atomically for unrelated readers.
2020
"""
2121

22-
from . import BOB_VERSION
2322
from .errors import BuildError
2423
from .tty import stepAction, stepMessage, \
2524
SKIPPED, EXECUTED, WARNING, INFO, TRACE, ERROR, IMPORTANT
26-
from .utils import asHexStr, removePath, isWindows, sslNoVerifyContext, \
27-
getBashPath, tarfileOpen
25+
from .utils import asHexStr, removePath, isWindows, getBashPath, tarfileOpen
26+
from .webdav import WebDav, HTTPException, HttpDownloadError, HttpUploadError, HttpNotFoundError, HttpAlreadyExistsError
2827
from shlex import quote
2928
from tempfile import mkstemp, NamedTemporaryFile, TemporaryFile, gettempdir
30-
import argparse
3129
import asyncio
32-
import base64
3330
import concurrent.futures
3431
import concurrent.futures.process
3532
import gzip
36-
import hashlib
3733
import http.client
3834
import io
3935
import os
4036
import os.path
4137
import shutil
4238
import signal
43-
import ssl
4439
import subprocess
4540
import tarfile
46-
import textwrap
4741
import urllib.parse
4842

4943
ARCHIVE_GENERATION = '-1'
@@ -390,7 +384,7 @@ def cachePackage(self, buildId, workspace):
390384
return self._openUploadFile(buildId, ARTIFACT_SUFFIX, False)
391385
except ArtifactExistsError:
392386
return None
393-
except (ArtifactUploadError, OSError) as e:
387+
except (ArtifactUploadError, HttpUploadError, OSError) as e:
394388
if self.__ignoreErrors:
395389
return None
396390
else:
@@ -406,10 +400,12 @@ def _downloadPackage(self, buildId, suffix, audit, content, caches, workspace):
406400
with Tee(name, fileobj, buildId, caches, workspace) as fo:
407401
self._extract(fo, audit, content)
408402
return (True, None, None)
409-
except ArtifactNotFoundError:
403+
except (ArtifactNotFoundError, HttpNotFoundError):
410404
return (False, "not found", WARNING)
411-
except ArtifactDownloadError as e:
405+
except (ArtifactDownloadError, HttpDownloadError) as e:
412406
return (False, e.reason, WARNING)
407+
except ConnectionRefusedError:
408+
return (False, "connection failed", WARNING)
413409
except BuildError as e:
414410
raise
415411
except OSError as e:
@@ -444,10 +440,12 @@ def _downloadLocalFile(self, key, suffix):
444440
with self._openDownloadFile(key, suffix) as (name, fileobj):
445441
ret = readFileOrHandle(name, fileobj)
446442
return (ret, None, None)
447-
except ArtifactNotFoundError:
443+
except (ArtifactNotFoundError, HttpNotFoundError):
448444
return (None, "not found", WARNING)
449-
except ArtifactDownloadError as e:
445+
except (ArtifactDownloadError, HttpDownloadError) as e:
450446
return (None, e.reason, WARNING)
447+
except ConnectionRefusedError:
448+
return (None, "connection failed", WARNING)
451449
except BuildError as e:
452450
raise
453451
except OSError as e:
@@ -487,9 +485,9 @@ def _uploadPackage(self, buildId, suffix, audit, content):
487485
try:
488486
with self._openUploadFile(buildId, suffix, False) as (name, fileobj):
489487
self._pack(name, fileobj, audit, content)
490-
except ArtifactExistsError:
488+
except (ArtifactExistsError, HttpAlreadyExistsError):
491489
return ("skipped ({} exists in archive)".format(content), SKIPPED)
492-
except (ArtifactUploadError, tarfile.TarError, OSError) as e:
490+
except (ArtifactUploadError, HttpUploadError, tarfile.TarError, OSError) as e:
493491
if self.__ignoreErrors:
494492
return ("error ("+str(e)+")", ERROR)
495493
else:
@@ -522,7 +520,7 @@ def _uploadLocalFile(self, key, suffix, content):
522520
# never see an ArtifactExistsError.
523521
with self._openUploadFile(key, suffix, True) as (name, fileobj):
524522
writeFileOrHandle(name, fileobj, content)
525-
except (ArtifactUploadError, OSError) as e:
523+
except (ArtifactUploadError, HttpUploadError, OSError) as e:
526524
if self.__ignoreErrors:
527525
return ("error ("+str(e)+")", ERROR)
528526
else:
@@ -737,118 +735,66 @@ def __exit__(self, exc_type, exc_value, traceback):
737735
return False
738736

739737

740-
class SimpleHttpArchive(BaseArchive):
738+
class HttpArchive(BaseArchive):
741739
def __init__(self, spec):
742740
super().__init__(spec)
743741
self.__url = urllib.parse.urlparse(spec["url"])
744-
self.__connection = None
745-
self.__sslVerify = spec.get("sslVerify", True)
742+
self._webdav = WebDav(self.__url, spec.get("sslVerify", True))
746743

747744
def __retry(self, request):
748745
retry = True
749746
while True:
750747
try:
751748
return (True, request())
752-
except (http.client.HTTPException, OSError) as e:
753-
self._resetConnection()
749+
except (HTTPException, OSError) as e:
750+
self._webdav._resetConnection()
754751
if not retry: return (False, e)
755752
retry = False
756753

757-
def _makeUrl(self, buildId, suffix):
754+
def _resetConnection(self):
755+
self._webdav._resetConnection()
756+
757+
def _makePath(self, buildId, suffix):
758758
packageResultId = buildIdToName(buildId)
759759
return "/".join([self.__url.path, packageResultId[0:2], packageResultId[2:4],
760760
packageResultId[4:] + suffix])
761761

762-
def _remoteName(self, buildId, suffix):
763-
url = self.__url
764-
return urllib.parse.urlunparse((url.scheme, url.netloc, self._makeUrl(buildId, suffix), '', '', ''))
765-
766-
def _getConnection(self):
767-
if self.__connection is not None:
768-
return self.__connection
769-
770-
url = self.__url
771-
if url.scheme == 'http':
772-
connection = http.client.HTTPConnection(url.hostname, url.port)
773-
elif url.scheme == 'https':
774-
ctx = None if self.__sslVerify else sslNoVerifyContext()
775-
connection = http.client.HTTPSConnection(url.hostname, url.port,
776-
context=ctx)
762+
def _makeParentDirs(self, path):
763+
(dirs, _, _) = path.rpartition("/")
764+
(ok, result) = self.__retry(lambda: self._webdav.mkdir(dirs, dirs.count("/")))
765+
if ok:
766+
return result
777767
else:
778-
raise BuildError("Unsupported URL scheme: '{}'".format(url.schema))
779-
780-
self.__connection = connection
781-
return connection
768+
raise result
782769

783-
def _resetConnection(self):
784-
if self.__connection is not None:
785-
self.__connection.close()
786-
self.__connection = None
787-
788-
def _getHeaders(self):
789-
headers = { 'User-Agent' : 'BobBuildTool/{}'.format(BOB_VERSION) }
790-
if self.__url.username is not None:
791-
username = urllib.parse.unquote(self.__url.username)
792-
passwd = urllib.parse.unquote(self.__url.password)
793-
userPass = username + ":" + passwd
794-
headers['Authorization'] = 'Basic ' + base64.b64encode(
795-
userPass.encode("utf-8")).decode("ascii")
796-
return headers
770+
def _remoteName(self, buildId, suffix):
771+
url = self.__url
772+
return urllib.parse.urlunparse((url.scheme, url.netloc, self._makePath(buildId, suffix), '', '', ''))
797773

798-
def _openDownloadFile(self, buildId, suffix):
799-
(ok, result) = self.__retry(lambda: self.__openDownloadFile(buildId, suffix))
774+
def _exists(self, path):
775+
(ok, result) = self.__retry(lambda: self._webdav.exists(path))
800776
if ok:
801777
return result
802778
else:
803-
raise ArtifactDownloadError(str(result))
779+
raise result
804780

805-
def __openDownloadFile(self, buildId, suffix):
806-
connection = self._getConnection()
807-
url = self._makeUrl(buildId, suffix)
808-
connection.request("GET", url, headers=self._getHeaders())
809-
response = connection.getresponse()
810-
if response.status == 200:
811-
return SimpleHttpDownloader(self, response)
812-
else:
813-
response.read()
814-
if response.status == 404:
815-
raise ArtifactNotFoundError()
816-
else:
817-
raise ArtifactDownloadError("{} {}".format(response.status,
818-
response.reason))
819-
820-
def _openUploadFile(self, buildId, suffix, overwrite):
821-
(ok, result) = self.__retry(lambda: self.__openUploadFile(buildId, suffix, overwrite))
781+
def _openDownloadFile(self, buildId, suffix):
782+
path = self._makePath(buildId, suffix)
783+
(ok, result) = self.__retry(lambda: self.__openDownloadFile(path))
822784
if ok:
823785
return result
824786
else:
825-
raise ArtifactUploadError(str(result))
787+
raise result
826788

827-
def __openUploadFile(self, buildId, suffix, overwrite):
828-
connection = self._getConnection()
829-
url = self._makeUrl(buildId, suffix)
789+
def __openDownloadFile(self, path):
790+
return HttpDownloader(self, self._webdav.download(path))
830791

792+
def _openUploadFile(self, buildId, suffix, overwrite):
793+
path = self._makePath(buildId, suffix)
831794
# check if already there
832795
if not overwrite:
833-
connection.request("HEAD", url, headers=self._getHeaders())
834-
response = connection.getresponse()
835-
response.read()
836-
if response.status == 200:
796+
if self._exists(path):
837797
raise ArtifactExistsError()
838-
elif response.status != 404:
839-
raise ArtifactUploadError("HEAD {} {}".format(response.status, response.reason))
840-
841-
# create temporary file
842-
return SimpleHttpUploader(self, url, overwrite)
843-
844-
def _putUploadFile(self, url, tmp, overwrite):
845-
(ok, result) = self.__retry(lambda: self.__putUploadFile(url, tmp, overwrite))
846-
if ok:
847-
return result
848-
else:
849-
raise ArtifactUploadError(str(result))
850-
851-
def __putUploadFile(self, url, tmp, overwrite):
852798
# Quoting RFC 4918:
853799
#
854800
# A PUT that would result in the creation of a resource without an
@@ -857,62 +803,18 @@ def __putUploadFile(self, url, tmp, overwrite):
857803
#
858804
# We don't want to waste bandwith by uploading big artifacts into a
859805
# missing directory. Thus make sure the directory always exists.
860-
self.__makeParentDirs(url)
861-
862-
# Determine file length outself and add a "Content-Length" header. This
863-
# used to work in Python 3.5 automatically but was removed later.
864-
tmp.seek(0, os.SEEK_END)
865-
length = str(tmp.tell())
866-
tmp.seek(0)
867-
headers = self._getHeaders()
868-
headers.update({ 'Content-Length' : length })
869-
if not overwrite:
870-
headers.update({ 'If-None-Match' : '*' })
871-
connection = self._getConnection()
872-
connection.request("PUT", url, tmp, headers=headers)
873-
response = connection.getresponse()
874-
response.read()
875-
if response.status == 412:
876-
# precondition failed -> lost race with other upload
877-
raise ArtifactExistsError()
878-
elif response.status not in [200, 201, 204]:
879-
raise ArtifactUploadError("PUT {} {}".format(response.status, response.reason))
806+
self._makeParentDirs(path)
807+
return HttpUploader(self, path, overwrite)
880808

881-
def __makeParentDirs(self, url, depth=0):
882-
"""Create parent directories.
809+
def _putUploadFile(self, path, tmp, overwrite):
810+
(ok, result) = self.__retry(lambda: self._webdav.upload(path, tmp, overwrite))
811+
if ok:
812+
return result
813+
else:
814+
raise result
883815

884-
Our artifacts are stored directory levels deep. Only do the MKCOL up
885-
to the base level.
886-
"""
887-
(dirs, _, _) = url.rpartition("/")
888-
if not dirs:
889-
return
890816

891-
response = self.__mkcol(dirs)
892-
if response.status == 409 and depth < 2:
893-
# 409 Conflict - Parent collection does not exist.
894-
self.__makeParentDirs(dirs, depth + 1)
895-
response = self.__mkcol(dirs)
896-
897-
# We expect to create the directory (201) or it already existed (405).
898-
# If the server does not support MKCOL we'd expect a 405 too and hope
899-
# for the best...
900-
if response.status not in [201, 405]:
901-
raise ArtifactUploadError("MKCOL {} {}".format(response.status, response.reason))
902-
903-
def __mkcol(self, url):
904-
# MKCOL resources must have a trailing slash because they are
905-
# directories. Otherwise Apache might send a HTTP 301. Nginx refuses to
906-
# create the directory with a 409 which looks odd.
907-
if not url.endswith("/"):
908-
url += "/"
909-
connection = self._getConnection()
910-
connection.request("MKCOL", url, headers=self._getHeaders())
911-
response = connection.getresponse()
912-
response.read()
913-
return response
914-
915-
class SimpleHttpDownloader:
817+
class HttpDownloader:
916818
def __init__(self, archiver, response):
917819
self.archiver = archiver
918820
self.response = response
@@ -924,7 +826,8 @@ def __exit__(self, exc_type, exc_value, traceback):
924826
self.archiver._resetConnection()
925827
return False
926828

927-
class SimpleHttpUploader:
829+
830+
class HttpUploader:
928831
def __init__(self, archiver, url, overwrite):
929832
self.archiver = archiver
930833
self.tmp = TemporaryFile()
@@ -1218,7 +1121,7 @@ def getSingleArchiver(recipes, archiveSpec):
12181121
if archiveBackend == "file":
12191122
return LocalArchive(archiveSpec)
12201123
elif archiveBackend == "http":
1221-
return SimpleHttpArchive(archiveSpec)
1124+
return HttpArchive(archiveSpec)
12221125
elif archiveBackend == "shell":
12231126
return CustomArchive(archiveSpec, recipes.envWhiteList())
12241127
elif archiveBackend == "azure":

0 commit comments

Comments
 (0)