document_download_service.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. """
  2. Document Download Permission and Statistics Service
  3. This service provides simple document download permission control and statistics tracking using Redis.
  4. All data is stored in Redis with automatic expiration.
  5. """
  6. import json
  7. import logging
  8. import time
  9. from datetime import datetime
  10. from typing import Any, Optional
  11. from extensions.ext_redis import redis_client, redis_fallback
  12. from models.account import Account
  13. logger = logging.getLogger(__name__)
  14. class DocumentDownloadService:
  15. """Document download permission and statistics service - simplified version"""
  16. # Redis Key prefixes
  17. PERMISSION_PREFIX = "doc:perm" # Permission configuration
  18. LOG_PREFIX = "doc:log" # Download logs
  19. COUNT_PREFIX = "doc:count" # Download count per user
  20. # Data retention time (90 days)
  21. RETENTION_DAYS = 90
  22. RETENTION_SECONDS = RETENTION_DAYS * 24 * 60 * 60
  23. # Maximum log entries per document
  24. MAX_LOG_ENTRIES = 1000
  25. # ========== Permission Management ==========
  26. @classmethod
  27. def _get_permission_key(cls, tenant_id: str, document_id: str) -> str:
  28. """Get Redis key for permission configuration."""
  29. return f"{cls.PERMISSION_PREFIX}:{tenant_id}:{document_id}"
  30. @classmethod
  31. @redis_fallback(default_return=True)
  32. def set_allowed_users(
  33. cls,
  34. tenant_id: str,
  35. document_id: str,
  36. user_ids: list[str],
  37. ) -> bool:
  38. """Set list of users allowed to download the document.
  39. Args:
  40. tenant_id: Tenant ID
  41. document_id: Document ID
  42. user_ids: List of user IDs allowed to download (empty list means everyone can download)
  43. Returns:
  44. Whether the operation was successful
  45. """
  46. key = cls._get_permission_key(tenant_id, document_id)
  47. # Delete old configuration
  48. redis_client.delete(key)
  49. # If user_ids is empty, no restriction is applied
  50. if not user_ids:
  51. logger.info(f"Document {document_id} permission restriction removed")
  52. return True
  53. # Add new user ID set
  54. redis_client.sadd(key, *user_ids)
  55. # Set expiration time
  56. redis_client.expire(key, cls.RETENTION_SECONDS)
  57. logger.info(f"Document {document_id} permission set for {len(user_ids)} users")
  58. return True
  59. @classmethod
  60. @redis_fallback(default_return=set())
  61. def get_allowed_users(
  62. cls,
  63. tenant_id: str,
  64. document_id: str,
  65. ) -> set[str]:
  66. """Get set of user IDs allowed to download.
  67. Returns:
  68. Set of user IDs. Empty set means no restriction.
  69. """
  70. key = cls._get_permission_key(tenant_id, document_id)
  71. members = redis_client.smembers(key)
  72. # Decode byte strings
  73. return {m.decode("utf-8") if isinstance(m, bytes) else m for m in members}
  74. @classmethod
  75. @redis_fallback(default_return=(True, None))
  76. def check_download_permission(
  77. cls,
  78. tenant_id: str,
  79. document_id: str,
  80. user_id: str,
  81. ) -> tuple[bool, Optional[str]]:
  82. """Check if user has download permission.
  83. Returns:
  84. (has_permission, error_message)
  85. """
  86. allowed_users = cls.get_allowed_users(tenant_id, document_id)
  87. # If no permission configured (empty set), everyone can download
  88. if not allowed_users:
  89. return True, None
  90. # Check if user is in allowed list
  91. if user_id not in allowed_users:
  92. logger.warning(f"User {user_id} denied download access to document {document_id}")
  93. return False, "You don't have permission to download this document"
  94. return True, None
  95. # ========== Download Logging ==========
  96. @classmethod
  97. def _get_log_key(cls, tenant_id: str, document_id: str) -> str:
  98. """Get Redis key for download logs."""
  99. return f"{cls.LOG_PREFIX}:{tenant_id}:{document_id}"
  100. @classmethod
  101. def _get_count_key(cls, tenant_id: str, document_id: str, user_id: str) -> str:
  102. """Get Redis key for user download count."""
  103. return f"{cls.COUNT_PREFIX}:{tenant_id}:{document_id}:{user_id}"
  104. @classmethod
  105. @redis_fallback(default_return=False)
  106. def record_download(
  107. cls,
  108. tenant_id: str,
  109. document_id: str,
  110. user_id: str,
  111. user_name: str,
  112. ip: str,
  113. ) -> bool:
  114. """Record a document download.
  115. Args:
  116. tenant_id: Tenant ID
  117. document_id: Document ID
  118. user_id: User ID
  119. user_name: User name
  120. ip: User IP address
  121. Returns:
  122. Whether the operation was successful
  123. """
  124. log_key = cls._get_log_key(tenant_id, document_id)
  125. # Create download record
  126. record = {
  127. "user_id": user_id,
  128. "user_name": user_name,
  129. "download_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  130. "ip": ip,
  131. }
  132. # Add to list head (newest first)
  133. redis_client.lpush(log_key, json.dumps(record, ensure_ascii=False))
  134. # Limit list length
  135. redis_client.ltrim(log_key, 0, cls.MAX_LOG_ENTRIES - 1)
  136. # Set expiration time
  137. redis_client.expire(log_key, cls.RETENTION_SECONDS)
  138. # Increment user download count
  139. count_key = cls._get_count_key(tenant_id, document_id, user_id)
  140. redis_client.incr(count_key)
  141. redis_client.expire(count_key, cls.RETENTION_SECONDS)
  142. logger.info(f"Recorded download: user={user_name}, document={document_id}, ip={ip}")
  143. return True
  144. @classmethod
  145. @redis_fallback(default_return=[])
  146. def get_download_logs(
  147. cls,
  148. tenant_id: str,
  149. document_id: str,
  150. limit: int = 100,
  151. ) -> list[dict[str, Any]]:
  152. """Get download logs for a document.
  153. Args:
  154. tenant_id: Tenant ID
  155. document_id: Document ID
  156. limit: Maximum number of records to return
  157. Returns:
  158. List of download records
  159. """
  160. log_key = cls._get_log_key(tenant_id, document_id)
  161. # Get latest N records
  162. records = redis_client.lrange(log_key, 0, limit - 1)
  163. result = []
  164. for record_json in records:
  165. try:
  166. if isinstance(record_json, bytes):
  167. record_json = record_json.decode("utf-8")
  168. record_data = json.loads(record_json)
  169. result.append(record_data)
  170. except Exception as e:
  171. logger.warning(f"Failed to parse download log: {e}")
  172. continue
  173. return result
  174. @classmethod
  175. @redis_fallback(default_return=0)
  176. def get_user_download_count(
  177. cls,
  178. tenant_id: str,
  179. document_id: str,
  180. user_id: str,
  181. ) -> int:
  182. """Get user's download count for a specific document."""
  183. count_key = cls._get_count_key(tenant_id, document_id, user_id)
  184. count = redis_client.get(count_key)
  185. return int(count) if count else 0
  186. @classmethod
  187. @redis_fallback(default_return=False)
  188. def clear_permission(
  189. cls,
  190. tenant_id: str,
  191. document_id: str,
  192. ) -> bool:
  193. """Clear download permission configuration for a document."""
  194. key = cls._get_permission_key(tenant_id, document_id)
  195. redis_client.delete(key)
  196. logger.info(f"Cleared permission for document {document_id}")
  197. return True