geek-cafe-saas-sdk 0.6.0__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.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,575 @@
1
+ """
2
+ FileSystemService for file CRUD operations with S3 and DynamoDB.
3
+
4
+ Geek Cafe, LLC
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, List
9
+ from boto3.dynamodb.conditions import Key
10
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
11
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
12
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
13
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
14
+ from geek_cafe_saas_sdk.core.error_codes import ErrorCode
15
+ from geek_cafe_saas_sdk.domains.files.models.file import File
16
+ from geek_cafe_saas_sdk.domains.files.services.s3_file_service import S3FileService
17
+ import os
18
+ from pathlib import Path
19
+
20
+
21
+ class FileSystemService(DatabaseService[File]):
22
+ """
23
+ File system service for managing files with S3 storage and DynamoDB metadata.
24
+
25
+ Handles:
26
+ - File uploads with metadata storage
27
+ - File downloads with access control
28
+ - File metadata CRUD operations
29
+ - Directory assignment
30
+ - Versioning strategy management
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ dynamodb: Optional[DynamoDB] = None,
37
+ table_name: Optional[str] = None,
38
+ s3_service: Optional[S3FileService] = None,
39
+ default_bucket: Optional[str] = None
40
+ ):
41
+ """
42
+ Initialize FileSystemService.
43
+
44
+ Args:
45
+ dynamodb: DynamoDB instance
46
+ table_name: DynamoDB table name
47
+ s3_service: S3FileService instance
48
+ default_bucket: Default S3 bucket
49
+ """
50
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
51
+ self.s3_service = s3_service or S3FileService(default_bucket=default_bucket)
52
+ self.default_bucket = default_bucket or os.getenv("S3_FILE_BUCKET")
53
+
54
+ def create(
55
+ self,
56
+ tenant_id: str,
57
+ user_id: str,
58
+ file_name: str,
59
+ file_data: bytes,
60
+ mime_type: str,
61
+ directory_id: Optional[str] = None,
62
+ versioning_strategy: str = "explicit",
63
+ description: Optional[str] = None,
64
+ tags: Optional[List[str]] = None,
65
+ **kwargs
66
+ ) -> ServiceResult[File]:
67
+ """
68
+ Upload a file with metadata.
69
+
70
+ Args:
71
+ tenant_id: Tenant ID
72
+ user_id: User ID (file owner)
73
+ file_name: File name
74
+ file_data: File content bytes
75
+ mime_type: MIME type
76
+ directory_id: Optional parent directory ID
77
+ versioning_strategy: "s3_native" or "explicit"
78
+ description: Optional description
79
+ tags: Optional tags
80
+
81
+ Returns:
82
+ ServiceResult with File model
83
+ """
84
+ try:
85
+ # Validate inputs
86
+ if not file_name:
87
+ raise ValidationError("File name is required", "file_name")
88
+
89
+ if not file_data:
90
+ raise ValidationError("File data is required", "file_data")
91
+
92
+ if versioning_strategy not in ["s3_native", "explicit"]:
93
+ raise ValidationError(
94
+ "Versioning strategy must be 's3_native' or 'explicit'",
95
+ "versioning_strategy"
96
+ )
97
+
98
+ # Create File model
99
+ file = File()
100
+ file.prep_for_save()
101
+ file.tenant_id = tenant_id
102
+ file.owner_id = user_id
103
+ file.file_name = file_name
104
+ file.mime_type = mime_type
105
+ file.file_size = len(file_data)
106
+ file.directory_id = directory_id
107
+ file.versioning_strategy = versioning_strategy
108
+ file.description = description
109
+ file.tags = tags or []
110
+ file.status = "active"
111
+
112
+ # Extract file extension
113
+ file_path = Path(file_name)
114
+ file.file_extension = file_path.suffix if file_path.suffix else None
115
+
116
+ # Build S3 key based on versioning strategy
117
+ if versioning_strategy == "s3_native":
118
+ # Same key for all versions - S3 handles versioning
119
+ s3_key = f"{tenant_id}/files/{file.file_id}/{file_name}"
120
+ else:
121
+ # Explicit versioning - unique key per version
122
+ version_id = file.id # Use file ID as first version ID
123
+ s3_key = f"{tenant_id}/files/{file.file_id}/versions/{version_id}/{file_name}"
124
+ file.current_version_id = version_id
125
+
126
+ file.s3_bucket = self.default_bucket
127
+ file.s3_key = s3_key
128
+ file.version_count = 1
129
+
130
+ # Build virtual path
131
+ if directory_id:
132
+ # TODO: Get directory path from DirectoryService
133
+ file.virtual_path = f"/{file_name}"
134
+ else:
135
+ file.virtual_path = f"/{file_name}"
136
+
137
+ # Upload to S3
138
+ upload_result = self.s3_service.upload_file(
139
+ file_data=file_data,
140
+ key=s3_key,
141
+ bucket=self.default_bucket
142
+ )
143
+
144
+ if not upload_result.success:
145
+ return ServiceResult.error_result(
146
+ message=f"Failed to upload file to S3: {upload_result.message}",
147
+ error_code=upload_result.error_code
148
+ )
149
+
150
+ # Save metadata to DynamoDB
151
+ pk = f"FILE#{tenant_id}#{file.file_id}"
152
+ sk = "METADATA"
153
+
154
+ item = file.to_dictionary()
155
+ item["pk"] = pk
156
+ item["sk"] = sk
157
+
158
+ # GSI1: Files by directory
159
+ item["gsi1_pk"] = f"TENANT#{tenant_id}"
160
+ if directory_id:
161
+ item["gsi1_sk"] = f"DIRECTORY#{directory_id}#{file_name}"
162
+ else:
163
+ item["gsi1_sk"] = f"DIRECTORY#ROOT#{file_name}"
164
+
165
+ # GSI2: Files by owner
166
+ item["gsi2_pk"] = f"TENANT#{tenant_id}#USER#{user_id}"
167
+ item["gsi2_sk"] = f"FILE#{file.created_utc_ts}"
168
+
169
+ self.dynamodb.save(
170
+ table_name=self.table_name,
171
+ item=item
172
+ )
173
+
174
+ return ServiceResult.success_result(file)
175
+
176
+ except ValidationError as e:
177
+ return ServiceResult.error_result(
178
+ message=str(e),
179
+ error_code=ErrorCode.VALIDATION_ERROR
180
+ )
181
+ except Exception as e:
182
+ return ServiceResult.exception_result(
183
+ exception=e,
184
+ error_code=ErrorCode.INTERNAL_ERROR,
185
+ context="FileSystemService.create"
186
+ )
187
+
188
+ def get_by_id(
189
+ self,
190
+ resource_id: str,
191
+ tenant_id: str,
192
+ user_id: str
193
+ ) -> ServiceResult[File]:
194
+ """
195
+ Get file by ID with access control.
196
+
197
+ Args:
198
+ resource_id: File ID
199
+ tenant_id: Tenant ID
200
+ user_id: User ID (for access control)
201
+
202
+ Returns:
203
+ ServiceResult with File model
204
+ """
205
+ try:
206
+ pk = f"FILE#{tenant_id}#{resource_id}"
207
+ sk = "METADATA"
208
+
209
+ result = self.dynamodb.get(
210
+ table_name=self.table_name,
211
+ key={"pk": pk, "sk": sk}
212
+ )
213
+
214
+ # Check if file exists first (before checking ownership)
215
+ # DynamoDB.get returns {'Item': {...}} or {'ResponseMetadata': {...}}
216
+ if not result or 'Item' not in result:
217
+ raise NotFoundError(f"File not found: {resource_id}")
218
+
219
+ # Convert to File model
220
+ file = File()
221
+ file.map(result['Item'])
222
+
223
+ # Access control: Check if user is owner or has share access
224
+ if file.owner_id != user_id:
225
+ # TODO: Check FileShare for access
226
+ raise AccessDeniedError("You do not have access to this file")
227
+
228
+ return ServiceResult.success_result(file)
229
+
230
+ except NotFoundError as e:
231
+ return ServiceResult.error_result(
232
+ message=str(e),
233
+ error_code=ErrorCode.NOT_FOUND
234
+ )
235
+ except AccessDeniedError as e:
236
+ return ServiceResult.error_result(
237
+ message=str(e),
238
+ error_code=ErrorCode.ACCESS_DENIED
239
+ )
240
+ except Exception as e:
241
+ return ServiceResult.exception_result(
242
+ exception=e,
243
+ error_code=ErrorCode.INTERNAL_ERROR,
244
+ context="FileSystemService.get_by_id"
245
+ )
246
+
247
+ def update(
248
+ self,
249
+ resource_id: str,
250
+ tenant_id: str,
251
+ user_id: str,
252
+ updates: Dict[str, Any]
253
+ ) -> ServiceResult[File]:
254
+ """
255
+ Update file metadata.
256
+
257
+ Args:
258
+ resource_id: File ID
259
+ tenant_id: Tenant ID
260
+ user_id: User ID (for access control)
261
+ updates: Dictionary of fields to update
262
+
263
+ Returns:
264
+ ServiceResult with updated File model
265
+ """
266
+ try:
267
+ # Get existing file
268
+ get_result = self.get_by_id(resource_id, tenant_id, user_id)
269
+ if not get_result.success:
270
+ return get_result
271
+
272
+ file = get_result.data
273
+
274
+ # Only owner can update
275
+ if file.owner_id != user_id:
276
+ raise AccessDeniedError("Only the owner can update this file")
277
+
278
+ # Apply updates (only allowed fields)
279
+ allowed_fields = [
280
+ "file_name", "description", "tags", "directory_id",
281
+ "status"
282
+ ]
283
+
284
+ for field, value in updates.items():
285
+ if field in allowed_fields:
286
+ setattr(file, field, value)
287
+
288
+ # Update timestamp
289
+ import datetime as dt
290
+ file.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
291
+
292
+ # Save to DynamoDB
293
+ pk = f"FILE#{tenant_id}#{resource_id}"
294
+ sk = "METADATA"
295
+
296
+ item = file.to_dictionary()
297
+ item["pk"] = pk
298
+ item["sk"] = sk
299
+
300
+ # Update GSI keys if directory changed
301
+ if "directory_id" in updates:
302
+ item["gsi1_pk"] = f"TENANT#{tenant_id}"
303
+ if updates["directory_id"]:
304
+ item["gsi1_sk"] = f"DIRECTORY#{updates['directory_id']}#{file.file_name}"
305
+ else:
306
+ item["gsi1_sk"] = f"DIRECTORY#ROOT#{file.file_name}"
307
+
308
+ self.dynamodb.save(
309
+ table_name=self.table_name,
310
+ item=item
311
+ )
312
+
313
+ return ServiceResult.success_result(file)
314
+
315
+ except AccessDeniedError as e:
316
+ return ServiceResult.error_result(
317
+ message=str(e),
318
+ error_code=ErrorCode.ACCESS_DENIED
319
+ )
320
+ except Exception as e:
321
+ return ServiceResult.exception_result(
322
+ exception=e,
323
+ error_code=ErrorCode.INTERNAL_ERROR,
324
+ context="FileSystemService.update"
325
+ )
326
+
327
+ def delete(
328
+ self,
329
+ resource_id: str,
330
+ tenant_id: str,
331
+ user_id: str,
332
+ hard_delete: bool = False
333
+ ) -> ServiceResult[bool]:
334
+ """
335
+ Delete file (soft or hard delete).
336
+
337
+ Args:
338
+ resource_id: File ID
339
+ tenant_id: Tenant ID
340
+ user_id: User ID (for access control)
341
+ hard_delete: If True, delete from S3 and DynamoDB. If False, mark as deleted.
342
+
343
+ Returns:
344
+ ServiceResult with success boolean
345
+ """
346
+ try:
347
+ # Get existing file
348
+ get_result = self.get_by_id(resource_id, tenant_id, user_id)
349
+ if not get_result.success:
350
+ return get_result
351
+
352
+ file = get_result.data
353
+
354
+ # Only owner can delete
355
+ if file.owner_id != user_id:
356
+ raise AccessDeniedError("Only the owner can delete this file")
357
+
358
+ if hard_delete:
359
+ # Delete from S3
360
+ if file.s3_key:
361
+ delete_result = self.s3_service.delete_file(
362
+ key=file.s3_key,
363
+ bucket=file.s3_bucket
364
+ )
365
+
366
+ if not delete_result.success:
367
+ return ServiceResult.error_result(
368
+ message=f"Failed to delete file from S3: {delete_result.message}",
369
+ error_code=delete_result.error_code
370
+ )
371
+
372
+ # Delete from DynamoDB
373
+ pk = f"FILE#{tenant_id}#{resource_id}"
374
+ sk = "METADATA"
375
+
376
+ self.dynamodb.delete(
377
+ primary_key={"pk": pk, "sk": sk},
378
+ table_name=self.table_name
379
+ )
380
+ else:
381
+ # Soft delete - update status
382
+ import datetime as dt
383
+ file.status = "deleted"
384
+ file.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
385
+
386
+ pk = f"FILE#{tenant_id}#{resource_id}"
387
+ sk = "METADATA"
388
+
389
+ item = file.to_dictionary()
390
+ item["pk"] = pk
391
+ item["sk"] = sk
392
+
393
+ self.dynamodb.save(
394
+ table_name=self.table_name,
395
+ item=item
396
+ )
397
+
398
+ return ServiceResult.success_result(True)
399
+
400
+ except AccessDeniedError as e:
401
+ return ServiceResult.error_result(
402
+ message=str(e),
403
+ error_code=ErrorCode.ACCESS_DENIED
404
+ )
405
+ except Exception as e:
406
+ return ServiceResult.exception_result(
407
+ exception=e,
408
+ error_code=ErrorCode.INTERNAL_ERROR,
409
+ context="FileSystemService.delete"
410
+ )
411
+
412
+ def download_file(
413
+ self,
414
+ file_id: str,
415
+ tenant_id: str,
416
+ user_id: str
417
+ ) -> ServiceResult[Dict[str, Any]]:
418
+ """
419
+ Download file with access control.
420
+
421
+ Args:
422
+ file_id: File ID
423
+ tenant_id: Tenant ID
424
+ user_id: User ID
425
+
426
+ Returns:
427
+ ServiceResult with file data and metadata
428
+ """
429
+ try:
430
+ # Get file metadata
431
+ get_result = self.get_by_id(file_id, tenant_id, user_id)
432
+ if not get_result.success:
433
+ return get_result
434
+
435
+ file = get_result.data
436
+
437
+ # Download from S3
438
+ download_result = self.s3_service.download_file(
439
+ key=file.s3_key,
440
+ bucket=file.s3_bucket
441
+ )
442
+
443
+ if not download_result.success:
444
+ return ServiceResult.error_result(
445
+ message=f"Failed to download file from S3: {download_result.message}",
446
+ error_code=download_result.error_code
447
+ )
448
+
449
+ # Combine file data with metadata
450
+ return ServiceResult.success_result({
451
+ "file": file,
452
+ "data": download_result.data["data"],
453
+ "content_type": download_result.data.get("content_type", file.mime_type),
454
+ "size": download_result.data.get("size", file.file_size)
455
+ })
456
+
457
+ except Exception as e:
458
+ return ServiceResult.exception_result(
459
+ exception=e,
460
+ error_code=ErrorCode.INTERNAL_ERROR,
461
+ context="FileSystemService.download_file"
462
+ )
463
+
464
+ def list_files_by_directory(
465
+ self,
466
+ tenant_id: str,
467
+ directory_id: Optional[str],
468
+ user_id: str,
469
+ limit: int = 50
470
+ ) -> ServiceResult[List[File]]:
471
+ """
472
+ List files in a directory.
473
+
474
+ Args:
475
+ tenant_id: Tenant ID
476
+ directory_id: Directory ID (None for root)
477
+ user_id: User ID (for access control)
478
+ limit: Maximum number of results
479
+
480
+ Returns:
481
+ ServiceResult with list of File models
482
+ """
483
+ try:
484
+ gsi1_pk = f"TENANT#{tenant_id}"
485
+
486
+ if directory_id:
487
+ gsi1_sk_prefix = f"DIRECTORY#{directory_id}#"
488
+ else:
489
+ gsi1_sk_prefix = "DIRECTORY#ROOT#"
490
+
491
+ # Query GSI1
492
+ results = self.dynamodb.query(
493
+ key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with(gsi1_sk_prefix),
494
+ table_name=self.table_name,
495
+ index_name="gsi1",
496
+ limit=limit
497
+ )
498
+
499
+ files = []
500
+ for item in results.get('Items', []):
501
+ file = File()
502
+ file.map(item)
503
+
504
+ # Filter out deleted files
505
+ if file.status != "deleted":
506
+ # Basic access control: show only owned files or shared files
507
+ # TODO: Check FileShare for shared access
508
+ if file.owner_id == user_id:
509
+ files.append(file)
510
+
511
+ return ServiceResult.success_result(files)
512
+
513
+ except Exception as e:
514
+ return ServiceResult.exception_result(
515
+ exception=e,
516
+ error_code=ErrorCode.INTERNAL_ERROR,
517
+ context="FileSystemService.list_files_by_directory"
518
+ )
519
+
520
+ def list_files_by_owner(
521
+ self,
522
+ tenant_id: str,
523
+ owner_id: str,
524
+ user_id: str,
525
+ limit: int = 50
526
+ ) -> ServiceResult[List[File]]:
527
+ """
528
+ List files owned by a user.
529
+
530
+ Args:
531
+ tenant_id: Tenant ID
532
+ owner_id: Owner user ID
533
+ user_id: Requesting user ID (for access control)
534
+ limit: Maximum number of results
535
+
536
+ Returns:
537
+ ServiceResult with list of File models
538
+ """
539
+ try:
540
+ # Can only list own files
541
+ if owner_id != user_id:
542
+ raise AccessDeniedError("You can only list your own files")
543
+
544
+ gsi2_pk = f"TENANT#{tenant_id}#USER#{owner_id}"
545
+
546
+ # Query GSI2
547
+ results = self.dynamodb.query(
548
+ key=Key('gsi2_pk').eq(gsi2_pk) & Key('gsi2_sk').begins_with("FILE#"),
549
+ table_name=self.table_name,
550
+ index_name="gsi2",
551
+ limit=limit
552
+ )
553
+
554
+ files = []
555
+ for item in results.get('Items', []):
556
+ file = File()
557
+ file.map(item)
558
+
559
+ # Filter out deleted files
560
+ if file.status != "deleted":
561
+ files.append(file)
562
+
563
+ return ServiceResult.success_result(files)
564
+
565
+ except AccessDeniedError as e:
566
+ return ServiceResult.error_result(
567
+ message=str(e),
568
+ error_code=ErrorCode.ACCESS_DENIED
569
+ )
570
+ except Exception as e:
571
+ return ServiceResult.exception_result(
572
+ exception=e,
573
+ error_code=ErrorCode.INTERNAL_ERROR,
574
+ context="FileSystemService.list_files_by_owner"
575
+ )