|  | @@ -22,9 +22,15 @@ try:
 | 
	
		
			
				|  |  |  except ImportError:
 | 
	
		
			
				|  |  |      lzma = None
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +try:
 | 
	
		
			
				|  |  | +    import zstd
 | 
	
		
			
				|  |  | +except ImportError:
 | 
	
		
			
				|  |  | +    zstd = None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  from .helpers import Buffer, DecompressionError
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -API_VERSION = '1.1_03'
 | 
	
		
			
				|  |  | +API_VERSION = '1.1_04'
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  cdef extern from "lz4.h":
 | 
	
		
			
				|  |  |      int LZ4_compress_limitedOutput(const char* source, char* dest, int inputSize, int maxOutputSize) nogil
 | 
	
	
		
			
				|  | @@ -186,6 +192,38 @@ class LZMA(CompressorBase):
 | 
	
		
			
				|  |  |              raise DecompressionError(str(e)) from None
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +class ZSTD(CompressorBase):
 | 
	
		
			
				|  |  | +    """zstd compression / decompression (pypi: zstandard, gh: python-zstandard)"""
 | 
	
		
			
				|  |  | +    # This is a NOT THREAD SAFE implementation.
 | 
	
		
			
				|  |  | +    # Only ONE python context must to be created at a time.
 | 
	
		
			
				|  |  | +    # It should work flawlessly as long as borg will call ONLY ONE compression job at time.
 | 
	
		
			
				|  |  | +    ID = b'\x03\x00'
 | 
	
		
			
				|  |  | +    name = 'zstd'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __init__(self, level=3, **kwargs):
 | 
	
		
			
				|  |  | +        super().__init__(**kwargs)
 | 
	
		
			
				|  |  | +        self.level = level
 | 
	
		
			
				|  |  | +        if zstd is None:
 | 
	
		
			
				|  |  | +            raise ValueError('No zstd support found.')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def compress(self, data):
 | 
	
		
			
				|  |  | +        if not isinstance(data, bytes):
 | 
	
		
			
				|  |  | +            data = bytes(data)  # zstd < 0.9.0 does not work with memoryview
 | 
	
		
			
				|  |  | +        cctx = zstd.ZstdCompressor(level=self.level, write_content_size=True)
 | 
	
		
			
				|  |  | +        data = cctx.compress(data)
 | 
	
		
			
				|  |  | +        return super().compress(data)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def decompress(self, data):
 | 
	
		
			
				|  |  | +        if not isinstance(data, bytes):
 | 
	
		
			
				|  |  | +            data = bytes(data)  # zstd < 0.9.0 does not work with memoryview
 | 
	
		
			
				|  |  | +        dctx = zstd.ZstdDecompressor()
 | 
	
		
			
				|  |  | +        data = super().decompress(data)
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            return dctx.decompress(data)
 | 
	
		
			
				|  |  | +        except zstd.ZstdError as e:
 | 
	
		
			
				|  |  | +            raise DecompressionError(str(e)) from None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  class ZLIB(CompressorBase):
 | 
	
		
			
				|  |  |      """
 | 
	
		
			
				|  |  |      zlib compression / decompression (python stdlib)
 | 
	
	
		
			
				|  | @@ -289,9 +327,10 @@ COMPRESSOR_TABLE = {
 | 
	
		
			
				|  |  |      ZLIB.name: ZLIB,
 | 
	
		
			
				|  |  |      LZMA.name: LZMA,
 | 
	
		
			
				|  |  |      Auto.name: Auto,
 | 
	
		
			
				|  |  | +    ZSTD.name: ZSTD,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  # List of possible compression types. Does not include Auto, since it is a meta-Compressor.
 | 
	
		
			
				|  |  | -COMPRESSOR_LIST = [LZ4, CNONE, ZLIB, LZMA, ]  # check fast stuff first
 | 
	
		
			
				|  |  | +COMPRESSOR_LIST = [LZ4, ZSTD, CNONE, ZLIB, LZMA, ]  # check fast stuff first
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  def get_compressor(name, **kwargs):
 | 
	
		
			
				|  |  |      cls = COMPRESSOR_TABLE[name]
 | 
	
	
		
			
				|  | @@ -344,6 +383,16 @@ class CompressionSpec:
 | 
	
		
			
				|  |  |              else:
 | 
	
		
			
				|  |  |                  raise ValueError
 | 
	
		
			
				|  |  |              self.level = level
 | 
	
		
			
				|  |  | +        elif self.name in ('zstd', ):
 | 
	
		
			
				|  |  | +            if count < 2:
 | 
	
		
			
				|  |  | +                level = 3  # default compression level in zstd
 | 
	
		
			
				|  |  | +            elif count == 2:
 | 
	
		
			
				|  |  | +                level = int(values[1])
 | 
	
		
			
				|  |  | +                if not 1 <= level <= 22:
 | 
	
		
			
				|  |  | +                    raise ValueError
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                raise ValueError
 | 
	
		
			
				|  |  | +            self.level = level
 | 
	
		
			
				|  |  |          elif self.name == 'auto':
 | 
	
		
			
				|  |  |              if 2 <= count <= 3:
 | 
	
		
			
				|  |  |                  compression = ','.join(values[1:])
 | 
	
	
		
			
				|  | @@ -357,7 +406,7 @@ class CompressionSpec:
 | 
	
		
			
				|  |  |      def compressor(self):
 | 
	
		
			
				|  |  |          if self.name in ('none', 'lz4', ):
 | 
	
		
			
				|  |  |              return get_compressor(self.name)
 | 
	
		
			
				|  |  | -        elif self.name in ('zlib', 'lzma', ):
 | 
	
		
			
				|  |  | +        elif self.name in ('zlib', 'lzma', 'zstd', ):
 | 
	
		
			
				|  |  |              return get_compressor(self.name, level=self.level)
 | 
	
		
			
				|  |  |          elif self.name == 'auto':
 | 
	
		
			
				|  |  |              return get_compressor(self.name, compressor=self.inner.compressor)
 |