tamar-file-hub-client 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- file_hub_client/client.py +24 -4
- file_hub_client/rpc/async_client.py +31 -4
- file_hub_client/rpc/gen/file_service_pb2.py +30 -12
- file_hub_client/rpc/gen/file_service_pb2_grpc.py +173 -0
- file_hub_client/rpc/interceptors.py +578 -580
- file_hub_client/rpc/protos/file_service.proto +68 -1
- file_hub_client/rpc/sync_client.py +31 -4
- file_hub_client/schemas/__init__.py +10 -0
- file_hub_client/schemas/context.py +171 -160
- file_hub_client/schemas/file.py +171 -126
- file_hub_client/services/file/async_blob_service.py +260 -8
- file_hub_client/services/file/async_file_service.py +217 -0
- file_hub_client/services/file/sync_blob_service.py +261 -8
- file_hub_client/services/file/sync_file_service.py +217 -0
- file_hub_client/utils/__init__.py +14 -0
- file_hub_client/utils/file_utils.py +186 -153
- file_hub_client/utils/ip_detector.py +226 -0
- file_hub_client/utils/logging.py +335 -318
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/METADATA +178 -2
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/RECORD +22 -21
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/WHEEL +0 -0
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,11 @@ from ...schemas import (
|
|
10
10
|
File,
|
11
11
|
FileListResponse,
|
12
12
|
GetFileResponse,
|
13
|
+
CompressionStatusResponse,
|
14
|
+
GetVariantsResponse,
|
15
|
+
RecompressionResponse,
|
16
|
+
VariantDownloadUrlResponse,
|
17
|
+
CompressedVariant,
|
13
18
|
)
|
14
19
|
from ...errors import FileNotFoundError
|
15
20
|
|
@@ -273,3 +278,215 @@ class SyncFileService(BaseFileService):
|
|
273
278
|
files = [self._convert_file_info(f) for f in response.files]
|
274
279
|
|
275
280
|
return FileListResponse(files=files)
|
281
|
+
|
282
|
+
def get_compression_status(
|
283
|
+
self,
|
284
|
+
file_id: str,
|
285
|
+
*,
|
286
|
+
request_id: Optional[str] = None,
|
287
|
+
**metadata
|
288
|
+
) -> CompressionStatusResponse:
|
289
|
+
"""
|
290
|
+
获取文件压缩状态
|
291
|
+
|
292
|
+
Args:
|
293
|
+
file_id: 文件ID
|
294
|
+
request_id: 请求ID,用于追踪
|
295
|
+
**metadata: 额外的gRPC元数据
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
CompressionStatusResponse: 压缩状态响应
|
299
|
+
"""
|
300
|
+
from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
|
301
|
+
|
302
|
+
stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
|
303
|
+
|
304
|
+
request = file_service_pb2.CompressionStatusRequest(file_id=file_id)
|
305
|
+
|
306
|
+
# 构建元数据
|
307
|
+
grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
|
308
|
+
|
309
|
+
response = stub.GetCompressionStatus(request, metadata=grpc_metadata)
|
310
|
+
|
311
|
+
# 转换压缩变体
|
312
|
+
variants = []
|
313
|
+
for variant in response.variants:
|
314
|
+
variants.append(CompressedVariant(
|
315
|
+
variant_name=variant.variant_name,
|
316
|
+
variant_type=variant.variant_type,
|
317
|
+
media_type=variant.media_type,
|
318
|
+
width=variant.width,
|
319
|
+
height=variant.height,
|
320
|
+
file_size=variant.file_size,
|
321
|
+
format=variant.format,
|
322
|
+
quality=variant.quality if variant.quality else None,
|
323
|
+
duration=variant.duration if variant.duration else None,
|
324
|
+
bitrate=variant.bitrate if variant.bitrate else None,
|
325
|
+
fps=variant.fps if variant.fps else None,
|
326
|
+
compression_ratio=variant.compression_ratio,
|
327
|
+
stored_path=variant.stored_path
|
328
|
+
))
|
329
|
+
|
330
|
+
return CompressionStatusResponse(
|
331
|
+
status=response.status,
|
332
|
+
error_message=response.error_message if response.error_message else None,
|
333
|
+
variants=variants
|
334
|
+
)
|
335
|
+
|
336
|
+
def get_compressed_variants(
|
337
|
+
self,
|
338
|
+
file_id: str,
|
339
|
+
*,
|
340
|
+
variant_type: Optional[str] = None,
|
341
|
+
request_id: Optional[str] = None,
|
342
|
+
**metadata
|
343
|
+
) -> GetVariantsResponse:
|
344
|
+
"""
|
345
|
+
获取文件的压缩变体
|
346
|
+
|
347
|
+
Args:
|
348
|
+
file_id: 文件ID
|
349
|
+
variant_type: 变体类型(image, video, thumbnail)
|
350
|
+
request_id: 请求ID,用于追踪
|
351
|
+
**metadata: 额外的gRPC元数据
|
352
|
+
|
353
|
+
Returns:
|
354
|
+
GetVariantsResponse: 压缩变体响应
|
355
|
+
"""
|
356
|
+
from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
|
357
|
+
|
358
|
+
stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
|
359
|
+
|
360
|
+
request = file_service_pb2.GetVariantsRequest(file_id=file_id)
|
361
|
+
if variant_type:
|
362
|
+
request.variant_type = variant_type
|
363
|
+
|
364
|
+
# 构建元数据
|
365
|
+
grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
|
366
|
+
|
367
|
+
response = stub.GetCompressedVariants(request, metadata=grpc_metadata)
|
368
|
+
|
369
|
+
# 转换压缩变体
|
370
|
+
variants = []
|
371
|
+
for variant in response.variants:
|
372
|
+
variants.append(CompressedVariant(
|
373
|
+
variant_name=variant.variant_name,
|
374
|
+
variant_type=variant.variant_type,
|
375
|
+
media_type=variant.media_type,
|
376
|
+
width=variant.width,
|
377
|
+
height=variant.height,
|
378
|
+
file_size=variant.file_size,
|
379
|
+
format=variant.format,
|
380
|
+
quality=variant.quality if variant.quality else None,
|
381
|
+
duration=variant.duration if variant.duration else None,
|
382
|
+
bitrate=variant.bitrate if variant.bitrate else None,
|
383
|
+
fps=variant.fps if variant.fps else None,
|
384
|
+
compression_ratio=variant.compression_ratio,
|
385
|
+
stored_path=variant.stored_path
|
386
|
+
))
|
387
|
+
|
388
|
+
return GetVariantsResponse(variants=variants)
|
389
|
+
|
390
|
+
def trigger_recompression(
|
391
|
+
self,
|
392
|
+
file_id: str,
|
393
|
+
*,
|
394
|
+
force_reprocess: bool = False,
|
395
|
+
request_id: Optional[str] = None,
|
396
|
+
**metadata
|
397
|
+
) -> RecompressionResponse:
|
398
|
+
"""
|
399
|
+
触发文件重新压缩
|
400
|
+
|
401
|
+
Args:
|
402
|
+
file_id: 文件ID
|
403
|
+
force_reprocess: 是否强制重新处理
|
404
|
+
request_id: 请求ID,用于追踪
|
405
|
+
**metadata: 额外的gRPC元数据
|
406
|
+
|
407
|
+
Returns:
|
408
|
+
RecompressionResponse: 重新压缩响应
|
409
|
+
"""
|
410
|
+
from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
|
411
|
+
|
412
|
+
stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
|
413
|
+
|
414
|
+
request = file_service_pb2.RecompressionRequest(
|
415
|
+
file_id=file_id,
|
416
|
+
force_reprocess=force_reprocess
|
417
|
+
)
|
418
|
+
|
419
|
+
# 构建元数据
|
420
|
+
grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
|
421
|
+
|
422
|
+
response = stub.TriggerRecompression(request, metadata=grpc_metadata)
|
423
|
+
|
424
|
+
return RecompressionResponse(
|
425
|
+
task_id=response.task_id,
|
426
|
+
status=response.status
|
427
|
+
)
|
428
|
+
|
429
|
+
def generate_variant_download_url(
|
430
|
+
self,
|
431
|
+
file_id: str,
|
432
|
+
variant_name: str,
|
433
|
+
*,
|
434
|
+
expire_seconds: int = 3600,
|
435
|
+
is_cdn: bool = False,
|
436
|
+
request_id: Optional[str] = None,
|
437
|
+
**metadata
|
438
|
+
) -> VariantDownloadUrlResponse:
|
439
|
+
"""
|
440
|
+
生成变体下载URL
|
441
|
+
|
442
|
+
Args:
|
443
|
+
file_id: 文件ID
|
444
|
+
variant_name: 变体名称(large/medium/small/thumbnail)
|
445
|
+
expire_seconds: 过期时间(秒)
|
446
|
+
is_cdn: 是否使用CDN
|
447
|
+
request_id: 请求ID,用于追踪
|
448
|
+
**metadata: 额外的gRPC元数据
|
449
|
+
|
450
|
+
Returns:
|
451
|
+
VariantDownloadUrlResponse: 变体下载URL响应
|
452
|
+
"""
|
453
|
+
from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
|
454
|
+
|
455
|
+
stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
|
456
|
+
|
457
|
+
request = file_service_pb2.VariantDownloadUrlRequest(
|
458
|
+
file_id=file_id,
|
459
|
+
variant_name=variant_name,
|
460
|
+
expire_seconds=expire_seconds,
|
461
|
+
is_cdn=is_cdn
|
462
|
+
)
|
463
|
+
|
464
|
+
# 构建元数据
|
465
|
+
grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
|
466
|
+
|
467
|
+
response = stub.GenerateVariantDownloadUrl(request, metadata=grpc_metadata)
|
468
|
+
|
469
|
+
# 转换变体信息
|
470
|
+
variant_info = None
|
471
|
+
if response.variant_info:
|
472
|
+
variant_info = CompressedVariant(
|
473
|
+
variant_name=response.variant_info.variant_name,
|
474
|
+
variant_type=response.variant_info.variant_type,
|
475
|
+
media_type=response.variant_info.media_type,
|
476
|
+
width=response.variant_info.width,
|
477
|
+
height=response.variant_info.height,
|
478
|
+
file_size=response.variant_info.file_size,
|
479
|
+
format=response.variant_info.format,
|
480
|
+
quality=response.variant_info.quality if response.variant_info.quality else None,
|
481
|
+
duration=response.variant_info.duration if response.variant_info.duration else None,
|
482
|
+
bitrate=response.variant_info.bitrate if response.variant_info.bitrate else None,
|
483
|
+
fps=response.variant_info.fps if response.variant_info.fps else None,
|
484
|
+
compression_ratio=response.variant_info.compression_ratio,
|
485
|
+
stored_path=response.variant_info.stored_path
|
486
|
+
)
|
487
|
+
|
488
|
+
return VariantDownloadUrlResponse(
|
489
|
+
url=response.url,
|
490
|
+
error=response.error if response.error else None,
|
491
|
+
variant_info=variant_info
|
492
|
+
)
|
@@ -46,6 +46,13 @@ from .logging import (
|
|
46
46
|
grpc_request_context,
|
47
47
|
log_grpc_call,
|
48
48
|
)
|
49
|
+
from .ip_detector import (
|
50
|
+
get_current_user_ip,
|
51
|
+
set_current_user_ip,
|
52
|
+
set_user_ip_extractor,
|
53
|
+
UserIPContext,
|
54
|
+
flask_auto_user_ip,
|
55
|
+
)
|
49
56
|
|
50
57
|
__all__ = [
|
51
58
|
# 文件工具
|
@@ -87,4 +94,11 @@ __all__ = [
|
|
87
94
|
"GrpcRequestLogger",
|
88
95
|
"grpc_request_context",
|
89
96
|
"log_grpc_call",
|
97
|
+
|
98
|
+
# IP检测工具
|
99
|
+
"get_current_user_ip",
|
100
|
+
"set_current_user_ip",
|
101
|
+
"set_user_ip_extractor",
|
102
|
+
"UserIPContext",
|
103
|
+
"flask_auto_user_ip",
|
90
104
|
]
|
@@ -1,153 +1,186 @@
|
|
1
|
-
"""
|
2
|
-
文件工具函数
|
3
|
-
"""
|
4
|
-
import hashlib
|
5
|
-
import mimetypes
|
6
|
-
from pathlib import Path
|
7
|
-
from typing import Generator, Optional, BinaryIO, Union
|
8
|
-
|
9
|
-
|
10
|
-
def get_file_mime_type(file_path: Union[str, Path]) -> str:
|
11
|
-
"""
|
12
|
-
获取文件的MIME类型
|
13
|
-
|
14
|
-
Args:
|
15
|
-
file_path: 文件路径
|
16
|
-
|
17
|
-
Returns:
|
18
|
-
MIME类型
|
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
|
-
|
1
|
+
"""
|
2
|
+
文件工具函数
|
3
|
+
"""
|
4
|
+
import hashlib
|
5
|
+
import mimetypes
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Generator, Optional, BinaryIO, Union
|
8
|
+
|
9
|
+
|
10
|
+
def get_file_mime_type(file_path: Union[str, Path]) -> str:
|
11
|
+
"""
|
12
|
+
获取文件的MIME类型
|
13
|
+
|
14
|
+
Args:
|
15
|
+
file_path: 文件路径
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
MIME类型
|
19
|
+
"""
|
20
|
+
import json
|
21
|
+
|
22
|
+
file_path = Path(file_path)
|
23
|
+
|
24
|
+
# 定义常见文件扩展名到MIME类型的映射,确保跨平台一致性
|
25
|
+
extension_mime_map = {
|
26
|
+
'.csv': 'text/csv',
|
27
|
+
'.txt': 'text/plain',
|
28
|
+
'.json': 'application/json',
|
29
|
+
'.xml': 'application/xml',
|
30
|
+
'.html': 'text/html',
|
31
|
+
'.htm': 'text/html',
|
32
|
+
'.pdf': 'application/pdf',
|
33
|
+
'.doc': 'application/msword',
|
34
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
35
|
+
'.xls': 'application/vnd.ms-excel',
|
36
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
37
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
38
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
39
|
+
'.jpg': 'image/jpeg',
|
40
|
+
'.jpeg': 'image/jpeg',
|
41
|
+
'.png': 'image/png',
|
42
|
+
'.gif': 'image/gif',
|
43
|
+
'.bmp': 'image/bmp',
|
44
|
+
'.webp': 'image/webp',
|
45
|
+
'.mp3': 'audio/mpeg',
|
46
|
+
'.wav': 'audio/wav',
|
47
|
+
'.mp4': 'video/mp4',
|
48
|
+
'.avi': 'video/x-msvideo',
|
49
|
+
'.mov': 'video/quicktime',
|
50
|
+
'.zip': 'application/zip',
|
51
|
+
'.rar': 'application/vnd.rar',
|
52
|
+
'.7z': 'application/x-7z-compressed',
|
53
|
+
'.tar': 'application/x-tar',
|
54
|
+
'.gz': 'application/gzip',
|
55
|
+
}
|
56
|
+
|
57
|
+
# 获取文件扩展名(转为小写)
|
58
|
+
extension = file_path.suffix.lower()
|
59
|
+
|
60
|
+
# 对于JSON文件,进行内容验证
|
61
|
+
if extension == '.json':
|
62
|
+
if file_path.exists():
|
63
|
+
try:
|
64
|
+
# 尝试不同的编码方式读取文件
|
65
|
+
content = None
|
66
|
+
for encoding in ['utf-8-sig', 'utf-8', 'latin-1']:
|
67
|
+
try:
|
68
|
+
with open(file_path, 'r', encoding=encoding) as f:
|
69
|
+
content = f.read().strip()
|
70
|
+
break
|
71
|
+
except UnicodeDecodeError:
|
72
|
+
continue
|
73
|
+
|
74
|
+
if content is None:
|
75
|
+
# 无法读取文件,返回text/plain
|
76
|
+
return 'text/plain'
|
77
|
+
|
78
|
+
if not content:
|
79
|
+
# 空文件,按扩展名处理
|
80
|
+
return extension_mime_map[extension]
|
81
|
+
|
82
|
+
# 尝试解析JSON
|
83
|
+
json.loads(content)
|
84
|
+
# 如果解析成功,确实是JSON格式
|
85
|
+
return 'application/json'
|
86
|
+
except (json.JSONDecodeError, OSError):
|
87
|
+
# JSON解析失败或文件读取失败,可能是格式错误的JSON文件
|
88
|
+
# 返回text/plain避免服务器端的类型不匹配错误
|
89
|
+
return 'text/plain'
|
90
|
+
|
91
|
+
# 优先使用自定义映射,确保常见文件类型的一致性
|
92
|
+
if extension in extension_mime_map:
|
93
|
+
return extension_mime_map[extension]
|
94
|
+
|
95
|
+
# 如果自定义映射中没有,尝试使用magic进行内容检测
|
96
|
+
try:
|
97
|
+
import magic
|
98
|
+
mime = magic.Magic(mime=True)
|
99
|
+
return mime.from_file(str(file_path))
|
100
|
+
except ImportError:
|
101
|
+
# 如果magic不可用,使用mimetypes作为fallback
|
102
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
103
|
+
return mime_type or "application/octet-stream"
|
104
|
+
|
105
|
+
|
106
|
+
def get_file_extension(file_name: str) -> str:
|
107
|
+
"""
|
108
|
+
获取文件扩展名
|
109
|
+
|
110
|
+
Args:
|
111
|
+
file_name: 文件名
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
文件扩展名(包含点号)
|
115
|
+
"""
|
116
|
+
return Path(file_name).suffix.lower()
|
117
|
+
|
118
|
+
|
119
|
+
def humanize_file_size(size_bytes: int) -> str:
|
120
|
+
"""
|
121
|
+
将文件大小转换为人类可读的格式
|
122
|
+
|
123
|
+
Args:
|
124
|
+
size_bytes: 文件大小(字节)
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
人类可读的文件大小
|
128
|
+
"""
|
129
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
130
|
+
if size_bytes < 1024.0:
|
131
|
+
return f"{size_bytes:.2f} {unit}"
|
132
|
+
size_bytes /= 1024.0
|
133
|
+
return f"{size_bytes:.2f} PB"
|
134
|
+
|
135
|
+
|
136
|
+
def calculate_file_hash(file_path: Union[str, Path], algorithm: str = "sha256") -> str:
|
137
|
+
"""
|
138
|
+
计算文件哈希值
|
139
|
+
|
140
|
+
Args:
|
141
|
+
file_path: 文件路径
|
142
|
+
algorithm: 哈希算法(md5, sha1, sha256等)
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
文件哈希值(十六进制)
|
146
|
+
"""
|
147
|
+
file_path = Path(file_path)
|
148
|
+
hash_obj = hashlib.new(algorithm)
|
149
|
+
|
150
|
+
with open(file_path, "rb") as f:
|
151
|
+
while chunk := f.read(8192):
|
152
|
+
hash_obj.update(chunk)
|
153
|
+
|
154
|
+
return hash_obj.hexdigest()
|
155
|
+
|
156
|
+
|
157
|
+
def split_file_chunks(
|
158
|
+
file_obj: BinaryIO,
|
159
|
+
chunk_size: int = 1024 * 1024, # 默认1MB
|
160
|
+
start_offset: int = 0
|
161
|
+
) -> Generator[tuple[bytes, int, bool], None, None]:
|
162
|
+
"""
|
163
|
+
将文件分割成块
|
164
|
+
|
165
|
+
Args:
|
166
|
+
file_obj: 文件对象
|
167
|
+
chunk_size: 块大小(字节)
|
168
|
+
start_offset: 起始偏移量
|
169
|
+
|
170
|
+
Yields:
|
171
|
+
(块数据, 偏移量, 是否最后一块)
|
172
|
+
"""
|
173
|
+
file_obj.seek(start_offset)
|
174
|
+
offset = start_offset
|
175
|
+
|
176
|
+
while True:
|
177
|
+
chunk = file_obj.read(chunk_size)
|
178
|
+
if not chunk:
|
179
|
+
break
|
180
|
+
|
181
|
+
is_last = len(chunk) < chunk_size
|
182
|
+
yield chunk, offset, is_last
|
183
|
+
|
184
|
+
offset += len(chunk)
|
185
|
+
if is_last:
|
186
|
+
break
|