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.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- 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
|
+
)
|