Skip to content

Commit ddc193e

Browse files
committed
feat: replace brotli with zstd compression
1 parent 5c7433f commit ddc193e

13 files changed

Lines changed: 90 additions & 89 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ COPY docker /app
2121
COPY requirements.d /app/requirements.d
2222

2323
RUN \
24-
# install python depends ---
24+
# install python build depends ---
2525
apk add --no-cache --virtual .build-deps build-base libffi-dev \
2626
# --- LDAP
2727
openldap-dev krb5-dev \

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ An asynchronous WebDAV server implementation, Support multi-provider, multi-acco
2525
- Passed all [litmus(0.13)](http://www.webdav.org/neon/litmus) test, except 3 warning
2626
- Browse the file directory in the browser
2727
- Support HTTP Basic/Digest authentication
28-
- Support response in Gzip/Brotli
28+
- Support response in Gzip/Zstd
2929
- Compatible with macOS finder and Window10 Explorer
3030

3131
## Quickstart

TODO

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
☐ 更新文档 @wip
1313
☐ 使用 TOML 方便添加注释的特性, 增加各种使用场景配置范例 @long-term
1414
☐ 以 TOML 下的可读性为标准,适当调整配置文件结构 @long-term
15+
☐ 重构 DAVResponse
16+
☐ 统一 401 的响应行为 @high @next-release
17+
☐ 浏览器请求才弹认证请求框, 服务器返回 401 时不弹认证框
18+
☐ 重构部分 DAVResponse 代码, 这个与压缩依赖部分一起修改
19+
☐ 顺带解决 litmus 的一个告警?
20+
✔ 移除 brotli 压缩方法 @done(25-11-14)
21+
✔ 添加sztd支持 @done(25-11-14)
1522
☐ 浏览器后台(内部)界面 @low
1623
☐ info界面
1724
☐ /_/admin/INDEX
@@ -48,12 +55,7 @@
4855

4956
待实现:
5057
☐ 基于配置,检查依赖安装情况,以便在启动时告警
51-
☐ 兼容 Py3.14
52-
☐ 重构 DAVResponse 来统一 401 的响应行为 @high @next-release
53-
☐ 浏览器请求才弹认证请求框, 服务器返回 401 时不弹认证框
54-
☐ 重构部分 DAVResponse 代码, 这个与压缩依赖部分一起修改
55-
☐ 顺带解决 litmus 的一个告警?
56-
☐ 移除第三方压缩依赖,添加sztd支持 @block dataclass-wizard 不兼容 py3.14
58+
☐ 兼容 Py3.14 @block dataclass-wizard 不兼容 py3.14
5759
☐ 更新 xmltodict 到最新版本
5860
这个上游库变化非常大, 之前版本与现在版本不兼容
5961
☐ 浏览器前台界面 @low

asgi_webdav/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ class TextFileCharsetDetect:
118118
class Compression:
119119
enable: bool = True
120120
enable_gzip: bool = True
121-
enable_brotli: bool = True
121+
enable_zstd: bool = True
122+
122123
level: DAVCompressLevel = DAVCompressLevel.RECOMMEND
123124

124125
content_type_user_rule: str = ""

asgi_webdav/constants.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -315,19 +315,7 @@ def __repr__(self):
315315

316316
RESPONSE_DATA_BLOCK_SIZE = 64 * 1024
317317

318-
319-
class DAVAcceptEncoding:
320-
# https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Encoding
321-
# https://caniuse.com/?search=gzip
322-
# identity
323-
gzip: bool = False
324-
br: bool = False
325-
326-
def __repr__(self):
327-
return f"gzip:{self.gzip}, br:{self.br}"
328-
329-
330-
DEFAULT_COMPRESSION_CONTENT_MINIMUM_LENGTH = 1000 # bytes
318+
DEFAULT_COMPRESSION_CONTENT_MINIMUM_LENGTH = 1024 # bytes
331319
DEFAULT_COMPRESSION_CONTENT_TYPE_RULE = r"^application/xml$|^text/"
332320

333321

asgi_webdav/request.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from asgi_webdav.constants import (
1111
DAV_PROPERTY_BASIC_KEYS,
12-
DAVAcceptEncoding,
1312
DAVDepth,
1413
DAVHeaders,
1514
DAVLockScope,
@@ -92,7 +91,7 @@ def path(self) -> DAVPath:
9291
authorization_method: str | None = None
9392

9493
# response info
95-
accept_encoding: DAVAcceptEncoding = field(default_factory=DAVAcceptEncoding)
94+
accept_encoding: str = ""
9695

9796
def __post_init__(self):
9897
self.method = self.scope.get("method", DAVMethod.UNKNOWN)
@@ -212,12 +211,10 @@ def __post_init__(self):
212211
else:
213212
self.lock_token = lock_token
214213

214+
# header: accept-encoding
215215
accept_encoding = self.headers.get(b"accept-encoding")
216216
if accept_encoding:
217-
if b"br" in accept_encoding:
218-
self.accept_encoding.br = True
219-
if b"gzip" in accept_encoding:
220-
self.accept_encoding.gzip = True
217+
self.accept_encoding = accept_encoding.decode("utf-8")
221218

222219
# header: range
223220
if self.method == DAVMethod.GET:

asgi_webdav/response.py

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
import gzip
33
import pprint
44
import re
5+
import sys
56
from collections.abc import AsyncGenerator
67
from enum import Enum
7-
from io import BytesIO
88
from logging import getLogger
99

10+
if sys.version_info >= (3, 14):
11+
import zstd
12+
else:
13+
from backports import zstd
14+
1015
from asgi_webdav.config import Config, get_config
1116
from asgi_webdav.constants import (
1217
DEFAULT_COMPRESSION_CONTENT_MINIMUM_LENGTH,
@@ -17,11 +22,6 @@
1722
from asgi_webdav.helpers import get_data_generator_from_content
1823
from asgi_webdav.request import DAVRequest
1924

20-
try:
21-
import brotli
22-
except ImportError:
23-
brotli = None
24-
2525
logger = getLogger(__name__)
2626

2727

@@ -32,9 +32,15 @@ class DAVResponseType(Enum):
3232

3333

3434
class DAVCompressionMethod(Enum):
35-
NONE = 0
36-
GZIP = 1
37-
BROTLI = 2
35+
"""
36+
Python 3.11 才支持 StrEnum
37+
然后使用 auto() 生成期望的枚举值
38+
并可以不使用 .value 做匹配
39+
"""
40+
41+
NONE = "none"
42+
GZIP = "gzip"
43+
ZSTD = "zstd"
3844

3945

4046
class DAVResponse:
@@ -44,7 +50,7 @@ class DAVResponse:
4450
headers: dict[bytes, bytes]
4551
compression_method: DAVCompressionMethod
4652

47-
def get_content(self):
53+
def get_content(self) -> AsyncGenerator:
4854
return self._content
4955

5056
def set_content(self, value: bytes | AsyncGenerator):
@@ -144,17 +150,23 @@ async def send_in_one_call(self, request: DAVRequest):
144150
config.compression.content_type_user_rule,
145151
):
146152
if (
147-
brotli is not None
148-
and config.compression.enable_brotli
149-
and request.accept_encoding.br
153+
config.compression.enable_zstd
154+
and DAVCompressionMethod.ZSTD.value in request.accept_encoding
150155
):
151-
self.compression_method = DAVCompressionMethod.BROTLI
152-
await BrotliSender(self, config.compression.level).send(request)
156+
self.compression_method = DAVCompressionMethod.ZSTD
157+
await CompressionSenderZstd(self, config.compression.level).send(
158+
request
159+
)
153160
return
154161

155-
if config.compression.enable_gzip and request.accept_encoding.gzip:
162+
if (
163+
config.compression.enable_gzip
164+
and DAVCompressionMethod.GZIP.value in request.accept_encoding
165+
):
156166
self.compression_method = DAVCompressionMethod.GZIP
157-
await GzipSender(self, config.compression.level).send(request)
167+
await CompressionSenderGzip(self, config.compression.level).send(
168+
request
169+
)
158170
return
159171

160172
self.compression_method = DAVCompressionMethod.NONE
@@ -228,12 +240,12 @@ class CompressionSenderAbc:
228240

229241
def __init__(self, response: DAVResponse):
230242
self.response = response
231-
self.buffer = BytesIO()
243+
# self.buffer = BytesIO()
232244

233-
def write(self, body: bytes):
245+
def compress(self, body: bytes) -> bytes:
234246
raise NotImplementedError
235247

236-
def close(self):
248+
def flush(self) -> bytes:
237249
raise NotImplementedError
238250

239251
async def send(self, request: DAVRequest):
@@ -250,13 +262,10 @@ async def send(self, request: DAVRequest):
250262
first = True
251263
async for body, more_body in self.response.content:
252264
# get and compress body
253-
self.write(body)
254-
if not more_body:
255-
self.close()
256-
body = self.buffer.getvalue()
257265

258-
self.buffer.seek(0)
259-
self.buffer.truncate()
266+
data = self.compress(body)
267+
if not more_body:
268+
data += self.flush()
260269

261270
if first:
262271
first = False
@@ -288,16 +297,17 @@ async def send(self, request: DAVRequest):
288297
await request.send(
289298
{
290299
"type": "http.response.body",
291-
"body": body,
300+
"body": data,
292301
"more_body": more_body,
293302
}
294303
)
295304

296305

297-
class GzipSender(CompressionSenderAbc):
306+
class CompressionSenderGzip(CompressionSenderAbc):
298307
"""
299308
https://en.wikipedia.org/wiki/Gzip
300309
https://developer.mozilla.org/en-US/docs/Glossary/GZip_compression
310+
https://docs.python.org/3.14/library/gzip.html
301311
"""
302312

303313
def __init__(self, response: DAVResponse, compress_level: DAVCompressLevel):
@@ -311,23 +321,21 @@ def __init__(self, response: DAVResponse, compress_level: DAVCompressLevel):
311321
level = 4
312322

313323
self.name = b"gzip"
314-
self.compressor = gzip.GzipFile(
315-
mode="wb", compresslevel=level, fileobj=self.buffer
316-
)
324+
self._level = level
317325

318-
def write(self, body: bytes):
319-
self.compressor.write(body)
326+
def compress(self, body: bytes) -> bytes:
327+
return gzip.compress(body, compresslevel=self._level)
320328

321-
def close(self):
322-
self.compressor.close()
329+
def flush(self) -> bytes:
330+
return b""
323331

324332

325-
class BrotliSender(CompressionSenderAbc):
333+
class CompressionSenderZstd(CompressionSenderAbc):
326334
"""
327-
https://datatracker.ietf.org/doc/html/rfc7932
328-
https://github.com/google/brotli
329-
https://caniuse.com/brotli
330-
https://developer.mozilla.org/en-US/docs/Glossary/brotli_compression
335+
https://en.wikipedia.org/wiki/Zstd
336+
https://facebook.github.io/zstd/
337+
https://developer.mozilla.org/en-US/docs/Glossary/Zstandard_compression
338+
https://docs.python.org/zh-cn/3.14/library/compression.zstd.html
331339
"""
332340

333341
def __init__(self, response: DAVResponse, compress_level: DAVCompressLevel):
@@ -336,19 +344,18 @@ def __init__(self, response: DAVResponse, compress_level: DAVCompressLevel):
336344
if compress_level == DAVCompressLevel.FAST:
337345
level = 1
338346
elif compress_level == DAVCompressLevel.BEST:
339-
level = 11
347+
level = 19
340348
else:
341-
level = 4
349+
level = 3 # compression.zstd.COMPRESSION_LEVEL_DEFAULT
342350

343-
self.name = b"br"
344-
self.compressor = brotli.Compressor(mode=brotli.MODE_TEXT, quality=level)
351+
self.name = b"zstd"
352+
self._compressor = zstd.ZstdCompressor(level=level)
345353

346-
def write(self, body: bytes):
347-
# https://github.com/google/brotli/blob/master/python/brotli.py
348-
self.buffer.write(self.compressor.process(body))
354+
def compress(self, body: bytes) -> bytes:
355+
return self._compressor.compress(body)
349356

350-
def close(self):
351-
self.buffer.write(self.compressor.finish())
357+
def flush(self) -> bytes:
358+
return self._compressor.flush()
352359

353360

354361
class DAVHideFileInDir:

docs/changelog.en.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 1.7.0
4+
5+
- Broken change:
6+
- remove brotli support
7+
- feat: support zstd compression in response
8+
39
## 1.6.1 - 20251101
410

511
- fix(docker): ldap depend miss

docs/index.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ An asynchronous WebDAV server implementation, Support multi-provider, multi-acco
2525
- Passed all [litmus(0.13)](http://www.webdav.org/neon/litmus) test, except 3 warning
2626
- Browse the file directory in the browser
2727
- Support HTTP Basic/Digest authentication
28-
- Support response in Gzip/Brotli
28+
- Support response in Gzip/Zstd
2929
- Compatible with macOS finder and Window10 Explorer
3030

3131
## Quick Start

docs/index.zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
- 通过 WebDAV 官方的 [litmus(0.13)](http://www.webdav.org/neon/litmus) 测试, 仅有两个警告
2626
- 可在浏览器中浏览文件目录
2727
- 支持 HTTP Basic/Digest 认证
28-
- 支持 Gzip/Brotli 压缩
28+
- 支持 Gzip/Zstd 压缩
2929
- 兼容 macOS 访达/ Window10
3030
Explorer [等客户端](https://rexzhang.github.io/asgi-webdav/compatibility/#compatible-clients)
3131

0 commit comments

Comments
 (0)