aiteamutils 0.2.144__py3-none-any.whl → 0.2.146__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.
- aiteamutils/base_service.py +25 -0
- aiteamutils/files.py +113 -68
- aiteamutils/version.py +1 -1
- {aiteamutils-0.2.144.dist-info → aiteamutils-0.2.146.dist-info}/METADATA +1 -1
- {aiteamutils-0.2.144.dist-info → aiteamutils-0.2.146.dist-info}/RECORD +6 -6
- {aiteamutils-0.2.144.dist-info → aiteamutils-0.2.146.dist-info}/WHEEL +0 -0
aiteamutils/base_service.py
CHANGED
@@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
6
|
from datetime import datetime
|
7
7
|
from ulid import ULID
|
8
8
|
from sqlalchemy import text
|
9
|
+
import logging
|
9
10
|
|
10
11
|
#패키지 라이브러리
|
11
12
|
from .exceptions import ErrorCode, CustomException
|
@@ -17,6 +18,8 @@ from .database import (
|
|
17
18
|
from .security import hash_password, verify_jwt_token, verify_role_permission
|
18
19
|
ModelType = TypeVar("ModelType", bound=DeclarativeBase)
|
19
20
|
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
20
23
|
class BaseService(Generic[ModelType]):
|
21
24
|
##################
|
22
25
|
# 초기화 영역 #
|
@@ -55,16 +58,23 @@ class BaseService(Generic[ModelType]):
|
|
55
58
|
Tuple[Dict[str, Any], Dict[str, Any]]: (처리된 엔티티 데이터, 파일 정보)
|
56
59
|
"""
|
57
60
|
try:
|
61
|
+
logger.info(f"[파일 처리 시작] operation: {operation}, storage_dir: {storage_dir}")
|
62
|
+
logger.info(f"[엔티티 데이터] {entity_data}")
|
63
|
+
|
58
64
|
entity_data_copy = entity_data.copy()
|
59
65
|
file_infos = {}
|
60
66
|
|
61
67
|
# 파일 데이터 분리
|
62
68
|
separated_files = {}
|
63
69
|
if operation != "delete" and 'extra_data' in entity_data_copy and isinstance(entity_data_copy['extra_data'], dict):
|
70
|
+
logger.info(f"[extra_data 확인] {entity_data_copy['extra_data']}")
|
64
71
|
extra_data = entity_data_copy['extra_data'].copy()
|
65
72
|
file_fields = {k: v for k, v in extra_data.items() if k.endswith('_files')}
|
73
|
+
logger.info(f"[파일 필드 확인] {file_fields}")
|
74
|
+
logger.info(f"[파일 필드 타입] {[(k, type(v), type(v[0]) if isinstance(v, list) and v else None) for k, v in file_fields.items()]}")
|
66
75
|
|
67
76
|
if file_fields and not storage_dir:
|
77
|
+
logger.error("[에러] storage_dir이 필요하지만 제공되지 않음")
|
68
78
|
raise CustomException(
|
69
79
|
ErrorCode.INVALID_INPUT,
|
70
80
|
detail="storage_dir is required for file upload",
|
@@ -74,6 +84,7 @@ class BaseService(Generic[ModelType]):
|
|
74
84
|
# 파일 필드 분리 및 제거
|
75
85
|
for field_name, files in file_fields.items():
|
76
86
|
if files:
|
87
|
+
logger.info(f"[파일 처리] 필드: {field_name}, 파일 수: {len(files)}")
|
77
88
|
separated_files[field_name] = files
|
78
89
|
del extra_data[field_name]
|
79
90
|
|
@@ -81,6 +92,7 @@ class BaseService(Generic[ModelType]):
|
|
81
92
|
|
82
93
|
# 기존 파일 삭제 (update 또는 delete 작업 시)
|
83
94
|
if operation in ["update", "delete"]:
|
95
|
+
logger.info("[기존 파일 삭제 시작]")
|
84
96
|
from .files import FileHandler
|
85
97
|
# files 테이블에서 기존 파일 정보 조회
|
86
98
|
existing_files = await self.db_session.execute(
|
@@ -96,9 +108,11 @@ class BaseService(Generic[ModelType]):
|
|
96
108
|
}
|
97
109
|
)
|
98
110
|
existing_files = existing_files.fetchall()
|
111
|
+
logger.info(f"[기존 파일 조회 결과] {existing_files}")
|
99
112
|
|
100
113
|
# 기존 파일 삭제
|
101
114
|
for file_info in existing_files:
|
115
|
+
logger.info(f"[파일 삭제] {file_info[0]}")
|
102
116
|
await FileHandler.delete_files(file_info[0])
|
103
117
|
|
104
118
|
# files 테이블에서 레코드 삭제
|
@@ -113,11 +127,14 @@ class BaseService(Generic[ModelType]):
|
|
113
127
|
"entity_ulid": entity_result.ulid
|
114
128
|
}
|
115
129
|
)
|
130
|
+
logger.info("[기존 파일 DB 레코드 삭제 완료]")
|
116
131
|
|
117
132
|
# 새 파일 저장 (create 또는 update 작업 시)
|
118
133
|
if operation != "delete" and separated_files:
|
134
|
+
logger.info("[새 파일 저장 시작]")
|
119
135
|
from .files import FileHandler
|
120
136
|
for field_name, files in separated_files.items():
|
137
|
+
logger.info(f"[필드 처리] {field_name}, 파일 수: {len(files)}")
|
121
138
|
saved_files = await FileHandler.save_files(
|
122
139
|
files=files,
|
123
140
|
storage_dir=storage_dir,
|
@@ -127,6 +144,7 @@ class BaseService(Generic[ModelType]):
|
|
127
144
|
column_name=field_name.replace('_files', '')
|
128
145
|
)
|
129
146
|
file_infos[field_name] = saved_files
|
147
|
+
logger.info(f"[파일 저장 완료] {field_name}: {len(saved_files)}개")
|
130
148
|
|
131
149
|
# extra_data 업데이트 - 파일 정보 캐싱
|
132
150
|
if not hasattr(entity_result, 'extra_data'):
|
@@ -144,17 +162,22 @@ class BaseService(Generic[ModelType]):
|
|
144
162
|
'column_name': f['column_name']
|
145
163
|
} for f in saved_files
|
146
164
|
]
|
165
|
+
logger.info(f"[extra_data 업데이트 완료] {field_name}")
|
147
166
|
|
148
167
|
# extra_data 업데이트된 엔티티 저장
|
149
168
|
await self.db_session.flush()
|
169
|
+
logger.info("[DB flush 완료]")
|
150
170
|
|
171
|
+
logger.info("[파일 처리 완료]")
|
151
172
|
return entity_data_copy, file_infos
|
152
173
|
|
153
174
|
except CustomException as e:
|
175
|
+
logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
|
154
176
|
if file_infos:
|
155
177
|
from .files import FileHandler
|
156
178
|
for file_list in file_infos.values():
|
157
179
|
for file_info in file_list:
|
180
|
+
logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
|
158
181
|
await FileHandler.delete_files(file_info["storage_path"])
|
159
182
|
raise CustomException(
|
160
183
|
e.error_code,
|
@@ -163,10 +186,12 @@ class BaseService(Generic[ModelType]):
|
|
163
186
|
original_error=e
|
164
187
|
)
|
165
188
|
except Exception as e:
|
189
|
+
logger.error(f"[예외 발생] {str(e)}")
|
166
190
|
if file_infos:
|
167
191
|
from .files import FileHandler
|
168
192
|
for file_list in file_infos.values():
|
169
193
|
for file_info in file_list:
|
194
|
+
logger.info(f"[에러 복구] 파일 삭제: {file_info['storage_path']}")
|
170
195
|
await FileHandler.delete_files(file_info["storage_path"])
|
171
196
|
raise CustomException(
|
172
197
|
ErrorCode.FILE_SYSTEM_ERROR,
|
aiteamutils/files.py
CHANGED
@@ -8,22 +8,39 @@ from mimetypes import guess_type
|
|
8
8
|
from sqlalchemy import text
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
10
10
|
from ulid import ULID
|
11
|
+
import logging
|
11
12
|
|
12
13
|
from .exceptions import ErrorCode, CustomException
|
14
|
+
from .base_model import BaseFileModel
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
13
17
|
|
14
18
|
class FileHandler:
|
15
19
|
"""파일 처리를 위한 핵심 기능 제공 클래스"""
|
16
20
|
|
17
21
|
@staticmethod
|
18
22
|
def _create_directory(directory: str) -> None:
|
19
|
-
"""디렉토리가 없는 경우 생성
|
20
|
-
|
21
|
-
Args:
|
22
|
-
directory (str): 생성할 디렉토리 경로
|
23
|
-
"""
|
23
|
+
"""디렉토리가 없는 경우 생성"""
|
24
24
|
try:
|
25
|
+
logger.info(f"[디렉토리 생성 시도] {directory}")
|
26
|
+
if os.path.exists(directory):
|
27
|
+
logger.info(f"[디렉토리 이미 존재] {directory}")
|
28
|
+
return
|
29
|
+
|
25
30
|
os.makedirs(directory, exist_ok=True)
|
31
|
+
logger.info(f"[디렉토리 생성 완료] {directory}")
|
32
|
+
|
33
|
+
# 권한 확인
|
34
|
+
if not os.access(directory, os.W_OK):
|
35
|
+
logger.error(f"[권한 에러] 디렉토리에 쓰기 권한 없음: {directory}")
|
36
|
+
raise CustomException(
|
37
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
38
|
+
detail=f"{directory}|No write permission",
|
39
|
+
source_function="FileHandler._create_directory"
|
40
|
+
)
|
41
|
+
|
26
42
|
except Exception as e:
|
43
|
+
logger.error(f"[디렉토리 생성 실패] {directory}: {str(e)}")
|
27
44
|
raise CustomException(
|
28
45
|
ErrorCode.FILE_SYSTEM_ERROR,
|
29
46
|
detail=f"{directory}|{str(e)}",
|
@@ -80,19 +97,10 @@ class FileHandler:
|
|
80
97
|
entity_name: str,
|
81
98
|
entity_ulid: str
|
82
99
|
) -> Dict[str, Any]:
|
83
|
-
"""파일을 저장하고 메타데이터 반환
|
84
|
-
|
85
|
-
Args:
|
86
|
-
file (BinaryIO): 저장할 파일 객체
|
87
|
-
original_name (str): 원본 파일명
|
88
|
-
storage_dir (str): 저장 디렉토리 경로
|
89
|
-
entity_name (str): 엔티티 이름
|
90
|
-
entity_ulid (str): 엔티티 ULID
|
91
|
-
|
92
|
-
Returns:
|
93
|
-
Dict[str, Any]: 저장된 파일의 메타데이터
|
94
|
-
"""
|
100
|
+
"""파일을 저장하고 메타데이터 반환"""
|
95
101
|
try:
|
102
|
+
logger.info(f"[파일 저장 시작] 원본 파일명: {original_name}, 저장 경로: {storage_dir}")
|
103
|
+
|
96
104
|
# 저장 디렉토리 생성
|
97
105
|
FileHandler._create_directory(storage_dir)
|
98
106
|
|
@@ -100,23 +108,30 @@ class FileHandler:
|
|
100
108
|
file_ext = os.path.splitext(original_name)[1]
|
101
109
|
storage_filename = f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{entity_ulid}{file_ext}"
|
102
110
|
storage_path = os.path.join(storage_dir, storage_filename)
|
111
|
+
logger.info(f"[저장 파일명] {storage_filename}")
|
103
112
|
|
104
113
|
# 파일 저장
|
105
114
|
current_position = file.tell()
|
106
115
|
file.seek(0)
|
107
116
|
|
108
|
-
|
109
|
-
|
110
|
-
|
117
|
+
try:
|
118
|
+
async with aiofiles.open(storage_path, 'wb') as f:
|
119
|
+
while chunk := file.read(8192):
|
120
|
+
await f.write(chunk)
|
121
|
+
logger.info(f"[파일 쓰기 완료] {storage_path}")
|
122
|
+
except Exception as e:
|
123
|
+
logger.error(f"[파일 쓰기 실패] {storage_path}: {str(e)}")
|
124
|
+
raise
|
111
125
|
|
112
126
|
# 체크섬 계산
|
113
127
|
file.seek(0)
|
114
128
|
checksum = await FileHandler._calculate_checksum(file)
|
129
|
+
logger.info(f"[체크섬 계산 완료] {checksum}")
|
115
130
|
|
116
131
|
# 파일 포인터 복구
|
117
132
|
file.seek(current_position)
|
118
133
|
|
119
|
-
|
134
|
+
file_info = {
|
120
135
|
"original_name": original_name,
|
121
136
|
"storage_path": storage_path,
|
122
137
|
"mime_type": FileHandler._get_mime_type(original_name),
|
@@ -125,9 +140,14 @@ class FileHandler:
|
|
125
140
|
"entity_name": entity_name,
|
126
141
|
"entity_ulid": entity_ulid
|
127
142
|
}
|
143
|
+
logger.info(f"[파일 정보] {file_info}")
|
144
|
+
return file_info
|
145
|
+
|
128
146
|
except CustomException as e:
|
147
|
+
logger.error(f"[CustomException 발생] {e.error_code}: {e.detail}")
|
129
148
|
raise e
|
130
149
|
except Exception as e:
|
150
|
+
logger.error(f"[파일 저장 실패] {str(e)}")
|
131
151
|
raise CustomException(
|
132
152
|
ErrorCode.FILE_SYSTEM_ERROR,
|
133
153
|
detail=f"{storage_path}|{str(e)}",
|
@@ -144,59 +164,84 @@ class FileHandler:
|
|
144
164
|
db_session: AsyncSession,
|
145
165
|
column_name: str = None
|
146
166
|
) -> List[Dict[str, Any]]:
|
147
|
-
"""파일(들)을 저장하고 메타데이터 반환
|
167
|
+
"""파일(들)을 저장하고 메타데이터 반환"""
|
168
|
+
logger.info(f"[다중 파일 저장 시작] storage_dir: {storage_dir}, entity_name: {entity_name}")
|
169
|
+
logger.info(f"[파일 데이터 타입] files type: {type(files)}")
|
170
|
+
if isinstance(files, list):
|
171
|
+
logger.info(f"[파일 리스트 내용] {[(type(f[0]), f[1]) for f in files]}")
|
172
|
+
else:
|
173
|
+
logger.info(f"[단일 파일 내용] {(type(files[0]), files[1])}")
|
148
174
|
|
149
|
-
Args:
|
150
|
-
files: 단일 파일 튜플 (file, original_name) 또는 파일 튜플 리스트
|
151
|
-
storage_dir (str): 저장 디렉토리 경로
|
152
|
-
entity_name (str): 엔티티 이름
|
153
|
-
entity_ulid (str): 엔티티 ULID
|
154
|
-
db_session (AsyncSession): DB 세션
|
155
|
-
column_name (str): 파일이 속한 컬럼 이름
|
156
|
-
|
157
|
-
Returns:
|
158
|
-
List[Dict[str, Any]]: 저장된 파일들의 메타데이터 리스트
|
159
|
-
"""
|
160
175
|
file_infos = []
|
161
176
|
# 단일 파일인 경우 리스트로 변환
|
162
177
|
files_list = [files] if isinstance(files, tuple) else files
|
178
|
+
logger.info(f"[처리할 파일 수] {len(files_list)}")
|
163
179
|
|
164
180
|
for file, original_name in files_list:
|
165
|
-
|
166
|
-
|
167
|
-
file
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
181
|
+
try:
|
182
|
+
logger.info(f"[개별 파일 처리 시작] {original_name}")
|
183
|
+
logger.info(f"[파일 객체 정보] type: {type(file)}, seekable: {file.seekable()}, readable: {file.readable()}")
|
184
|
+
|
185
|
+
# 파일 저장 및 메타데이터 생성
|
186
|
+
file_info = await FileHandler._save_file(
|
187
|
+
file=file,
|
188
|
+
original_name=original_name,
|
189
|
+
storage_dir=storage_dir,
|
190
|
+
entity_name=entity_name,
|
191
|
+
entity_ulid=entity_ulid
|
192
|
+
)
|
193
|
+
|
194
|
+
# DB에 파일 정보 저장
|
195
|
+
try:
|
196
|
+
logger.info(f"[DB 저장 시작] {original_name}")
|
197
|
+
|
198
|
+
# FileModel 인스턴스 생성
|
199
|
+
file_record = BaseFileModel(
|
200
|
+
entity_name=entity_name,
|
201
|
+
entity_ulid=entity_ulid,
|
202
|
+
original_name=file_info["original_name"],
|
203
|
+
storage_path=file_info["storage_path"],
|
204
|
+
mime_type=file_info["mime_type"],
|
205
|
+
size=file_info["size"],
|
206
|
+
checksum=file_info["checksum"],
|
207
|
+
column_name=column_name
|
208
|
+
)
|
209
|
+
|
210
|
+
# DB에 저장
|
211
|
+
db_session.add(file_record)
|
212
|
+
await db_session.flush()
|
213
|
+
|
214
|
+
# 결과를 딕셔너리로 변환
|
215
|
+
file_info = {
|
216
|
+
"ulid": file_record.ulid,
|
217
|
+
"entity_name": file_record.entity_name,
|
218
|
+
"entity_ulid": file_record.entity_ulid,
|
219
|
+
"original_name": file_record.original_name,
|
220
|
+
"storage_path": file_record.storage_path,
|
221
|
+
"mime_type": file_record.mime_type,
|
222
|
+
"size": file_record.size,
|
223
|
+
"checksum": file_record.checksum,
|
224
|
+
"column_name": file_record.column_name
|
225
|
+
}
|
226
|
+
|
227
|
+
file_infos.append(file_info)
|
228
|
+
logger.info(f"[DB 저장 완료] {original_name}, ulid: {file_record.ulid}")
|
229
|
+
|
230
|
+
except Exception as e:
|
231
|
+
logger.error(f"[DB 저장 실패] {original_name}: {str(e)}")
|
232
|
+
# 파일 삭제 시도
|
233
|
+
try:
|
234
|
+
if "storage_path" in file_info:
|
235
|
+
await FileHandler.delete_files(file_info["storage_path"])
|
236
|
+
logger.info(f"[저장 실패로 인한 파일 삭제] {file_info['storage_path']}")
|
237
|
+
except Exception as del_e:
|
238
|
+
logger.error(f"[파일 삭제 실패] {str(del_e)}")
|
239
|
+
raise
|
240
|
+
except Exception as e:
|
241
|
+
logger.error(f"[파일 처리 실패] {original_name}: {str(e)}")
|
242
|
+
raise
|
243
|
+
|
244
|
+
logger.info(f"[다중 파일 저장 완료] 성공: {len(file_infos)}개")
|
200
245
|
return file_infos
|
201
246
|
|
202
247
|
@staticmethod
|
aiteamutils/version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
"""버전 정보"""
|
2
|
-
__version__ = "0.2.
|
2
|
+
__version__ = "0.2.146"
|
@@ -1,16 +1,16 @@
|
|
1
1
|
aiteamutils/__init__.py,sha256=kRBpRjark0M8ZwFfmKiMFol6CbIILN3WE4f6_P6iIq0,1089
|
2
2
|
aiteamutils/base_model.py,sha256=yBZqzTDF9PA4wCAvmYfG12FdVwLtxOEUCcA3z2i6fXU,4176
|
3
3
|
aiteamutils/base_repository.py,sha256=Oy2zE1i5qx60Xf1tnsaKLyFWapiPqt5JH8NejwNrPWg,4647
|
4
|
-
aiteamutils/base_service.py,sha256=
|
4
|
+
aiteamutils/base_service.py,sha256=r7sAETymMMvKUjYFo8To3EnnIIOPFQ7PGI_IzuNx5-o,22504
|
5
5
|
aiteamutils/cache.py,sha256=07xBGlgAwOTAdY5mnMOQJ5EBxVwe8glVD7DkGEkxCtw,1373
|
6
6
|
aiteamutils/config.py,sha256=YdalpJb70-txhGJAS4aaKglEZAFVWgfzw5BXSWpkUz4,3232
|
7
7
|
aiteamutils/database.py,sha256=msvBKtxWeQVOo0v2Q9i2azuTNtnUItuNNar52gdRZTo,20418
|
8
8
|
aiteamutils/enums.py,sha256=7WLqlcJqQWtETAga2WAxNp3dJTQIAd2TW-4WzkoHHa8,2498
|
9
9
|
aiteamutils/exceptions.py,sha256=pgf3ersezObyl17wAO3I2fb8m9t2OzWDX1mSjwAWm2Y,16035
|
10
|
-
aiteamutils/files.py,sha256=
|
10
|
+
aiteamutils/files.py,sha256=Kvm3nIUZ6NJjjTvHX-sXeLTxgH-k1jJq_Twqvur14Hw,11823
|
11
11
|
aiteamutils/security.py,sha256=McUl3t5Z5SyUDVUHymHdDkYyF4YSeg4g9fFMML4W6Kw,11630
|
12
12
|
aiteamutils/validators.py,sha256=_WHN6jqJQzKM5uPTg-Da8U2qqevS84XeKMkCCF4C_lY,9591
|
13
|
-
aiteamutils/version.py,sha256=
|
14
|
-
aiteamutils-0.2.
|
15
|
-
aiteamutils-0.2.
|
16
|
-
aiteamutils-0.2.
|
13
|
+
aiteamutils/version.py,sha256=ClTCT30rXE2oKBHZRmAcdozSNQ3LRVgasEyLlocl1QA,43
|
14
|
+
aiteamutils-0.2.146.dist-info/METADATA,sha256=vHpFbI0TxUminJyv0a0Hs5TPhslR8nh9LqnojZ-Ujgc,1743
|
15
|
+
aiteamutils-0.2.146.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
16
|
+
aiteamutils-0.2.146.dist-info/RECORD,,
|
File without changes
|