1919concurrent uploads the artifact must appear atomically for unrelated readers.
2020"""
2121
22- from . import BOB_VERSION
2322from .errors import BuildError
2423from .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
2827from shlex import quote
2928from tempfile import mkstemp , NamedTemporaryFile , TemporaryFile , gettempdir
30- import argparse
3129import asyncio
32- import base64
3330import concurrent .futures
3431import concurrent .futures .process
3532import gzip
36- import hashlib
3733import http .client
3834import io
3935import os
4036import os .path
4137import shutil
4238import signal
43- import ssl
4439import subprocess
4540import tarfile
46- import textwrap
4741import urllib .parse
4842
4943ARCHIVE_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