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.
@@ -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
- file_path = Path(file_path)
21
-
22
- # 定义常见文件扩展名到MIME类型的映射,确保跨平台一致性
23
- extension_mime_map = {
24
- '.csv': 'text/csv',
25
- '.txt': 'text/plain',
26
- '.json': 'application/json',
27
- '.xml': 'application/xml',
28
- '.html': 'text/html',
29
- '.htm': 'text/html',
30
- '.pdf': 'application/pdf',
31
- '.doc': 'application/msword',
32
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
33
- '.xls': 'application/vnd.ms-excel',
34
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
35
- '.ppt': 'application/vnd.ms-powerpoint',
36
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
37
- '.jpg': 'image/jpeg',
38
- '.jpeg': 'image/jpeg',
39
- '.png': 'image/png',
40
- '.gif': 'image/gif',
41
- '.bmp': 'image/bmp',
42
- '.webp': 'image/webp',
43
- '.mp3': 'audio/mpeg',
44
- '.wav': 'audio/wav',
45
- '.mp4': 'video/mp4',
46
- '.avi': 'video/x-msvideo',
47
- '.mov': 'video/quicktime',
48
- '.zip': 'application/zip',
49
- '.rar': 'application/vnd.rar',
50
- '.7z': 'application/x-7z-compressed',
51
- '.tar': 'application/x-tar',
52
- '.gz': 'application/gzip',
53
- }
54
-
55
- # 获取文件扩展名(转为小写)
56
- extension = file_path.suffix.lower()
57
-
58
- # 优先使用自定义映射,确保常见文件类型的一致性
59
- if extension in extension_mime_map:
60
- return extension_mime_map[extension]
61
-
62
- # 如果自定义映射中没有,尝试使用magic进行内容检测
63
- try:
64
- import magic
65
- mime = magic.Magic(mime=True)
66
- return mime.from_file(str(file_path))
67
- except ImportError:
68
- # 如果magic不可用,使用mimetypes作为fallback
69
- mime_type, _ = mimetypes.guess_type(str(file_path))
70
- return mime_type or "application/octet-stream"
71
-
72
-
73
- def get_file_extension(file_name: str) -> str:
74
- """
75
- 获取文件扩展名
76
-
77
- Args:
78
- file_name: 文件名
79
-
80
- Returns:
81
- 文件扩展名(包含点号)
82
- """
83
- return Path(file_name).suffix.lower()
84
-
85
-
86
- def humanize_file_size(size_bytes: int) -> str:
87
- """
88
- 将文件大小转换为人类可读的格式
89
-
90
- Args:
91
- size_bytes: 文件大小(字节)
92
-
93
- Returns:
94
- 人类可读的文件大小
95
- """
96
- for unit in ["B", "KB", "MB", "GB", "TB"]:
97
- if size_bytes < 1024.0:
98
- return f"{size_bytes:.2f} {unit}"
99
- size_bytes /= 1024.0
100
- return f"{size_bytes:.2f} PB"
101
-
102
-
103
- def calculate_file_hash(file_path: Union[str, Path], algorithm: str = "sha256") -> str:
104
- """
105
- 计算文件哈希值
106
-
107
- Args:
108
- file_path: 文件路径
109
- algorithm: 哈希算法(md5, sha1, sha256等)
110
-
111
- Returns:
112
- 文件哈希值(十六进制)
113
- """
114
- file_path = Path(file_path)
115
- hash_obj = hashlib.new(algorithm)
116
-
117
- with open(file_path, "rb") as f:
118
- while chunk := f.read(8192):
119
- hash_obj.update(chunk)
120
-
121
- return hash_obj.hexdigest()
122
-
123
-
124
- def split_file_chunks(
125
- file_obj: BinaryIO,
126
- chunk_size: int = 1024 * 1024, # 默认1MB
127
- start_offset: int = 0
128
- ) -> Generator[tuple[bytes, int, bool], None, None]:
129
- """
130
- 将文件分割成块
131
-
132
- Args:
133
- file_obj: 文件对象
134
- chunk_size: 块大小(字节)
135
- start_offset: 起始偏移量
136
-
137
- Yields:
138
- (块数据, 偏移量, 是否最后一块)
139
- """
140
- file_obj.seek(start_offset)
141
- offset = start_offset
142
-
143
- while True:
144
- chunk = file_obj.read(chunk_size)
145
- if not chunk:
146
- break
147
-
148
- is_last = len(chunk) < chunk_size
149
- yield chunk, offset, is_last
150
-
151
- offset += len(chunk)
152
- if is_last:
153
- break
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