fastapi-rtk 1.0.19__py3-none-any.whl → 1.0.20__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.
- fastapi_rtk/__init__.py +4 -1
- fastapi_rtk/_version.py +1 -1
- fastapi_rtk/api/model_rest_api.py +195 -117
- fastapi_rtk/bases/__init__.py +2 -0
- fastapi_rtk/bases/file_manager.py +82 -10
- fastapi_rtk/file_managers/file_manager.py +1 -0
- fastapi_rtk/file_managers/image_manager.py +1 -0
- fastapi_rtk/file_managers/s3_file_manager.py +30 -13
- fastapi_rtk/file_managers/s3_image_manager.py +5 -0
- fastapi_rtk/lang/messages.pot +33 -28
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +33 -26
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +31 -26
- fastapi_rtk/setting.py +8 -0
- fastapi_rtk/utils/__init__.py +4 -3
- fastapi_rtk/utils/async_task_runner.py +27 -2
- fastapi_rtk/utils/{prettify_dict.py → formatter.py} +23 -1
- fastapi_rtk/utils/run_utils.py +3 -2
- {fastapi_rtk-1.0.19.dist-info → fastapi_rtk-1.0.20.dist-info}/METADATA +1 -1
- {fastapi_rtk-1.0.19.dist-info → fastapi_rtk-1.0.20.dist-info}/RECORD +24 -24
- {fastapi_rtk-1.0.19.dist-info → fastapi_rtk-1.0.20.dist-info}/WHEEL +0 -0
- {fastapi_rtk-1.0.19.dist-info → fastapi_rtk-1.0.20.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-1.0.19.dist-info → fastapi_rtk-1.0.20.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/__init__.py
CHANGED
|
@@ -130,7 +130,9 @@ __all__ = [
|
|
|
130
130
|
# .bases
|
|
131
131
|
"DBQueryParams",
|
|
132
132
|
"AbstractQueryBuilder",
|
|
133
|
+
"BaseFileException",
|
|
133
134
|
"FileNotAllowedException",
|
|
135
|
+
"FileTooLargeException",
|
|
134
136
|
"AbstractFileManager",
|
|
135
137
|
"AbstractImageManager",
|
|
136
138
|
"AbstractBaseFilter",
|
|
@@ -231,13 +233,14 @@ __all__ = [
|
|
|
231
233
|
"ExtenderMixin",
|
|
232
234
|
"uuid_namegen",
|
|
233
235
|
"secure_filename",
|
|
236
|
+
"prettify_dict",
|
|
237
|
+
"format_file_size",
|
|
234
238
|
"hooks",
|
|
235
239
|
"lazy",
|
|
236
240
|
"lazy_import",
|
|
237
241
|
"lazy_self",
|
|
238
242
|
"merge_schema",
|
|
239
243
|
"multiple_async_contexts",
|
|
240
|
-
"prettify_dict",
|
|
241
244
|
"generate_schema_from_typed_dict",
|
|
242
245
|
"get_pydantic_model_field",
|
|
243
246
|
"smart_run",
|
fastapi_rtk/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
1
|
+
__version__ = "1.0.20"
|
|
@@ -47,6 +47,7 @@ from ..utils import (
|
|
|
47
47
|
SelfType,
|
|
48
48
|
T,
|
|
49
49
|
deep_merge,
|
|
50
|
+
format_file_size,
|
|
50
51
|
lazy_self,
|
|
51
52
|
merge_schema,
|
|
52
53
|
smart_run,
|
|
@@ -1681,29 +1682,36 @@ class ModelRestApi(BaseApi):
|
|
|
1681
1682
|
If you are overriding this method, make sure to copy all the decorators too.
|
|
1682
1683
|
"""
|
|
1683
1684
|
async with AsyncTaskRunner():
|
|
1684
|
-
async with AsyncTaskRunner(
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
item
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1685
|
+
async with AsyncTaskRunner(
|
|
1686
|
+
"after_commit", run_tasks_even_if_exception=True
|
|
1687
|
+
) as after_commit_runner:
|
|
1688
|
+
async with AsyncTaskRunner("before_commit") as before_commit_runner:
|
|
1689
|
+
body_json = await smart_run(
|
|
1690
|
+
self._process_body,
|
|
1691
|
+
session,
|
|
1692
|
+
body,
|
|
1693
|
+
self.add_query_rel_fields,
|
|
1694
|
+
self.add_schema_extra_fields.keys()
|
|
1695
|
+
if self.add_schema_extra_fields
|
|
1696
|
+
else None,
|
|
1697
|
+
)
|
|
1698
|
+
item = self.datamodel.obj(**body_json)
|
|
1699
|
+
pre_add = await smart_run(
|
|
1700
|
+
self.pre_add,
|
|
1701
|
+
item,
|
|
1702
|
+
PARAM_BODY_SESSION(body=body, session=session),
|
|
1703
|
+
)
|
|
1704
|
+
if pre_add is not None:
|
|
1705
|
+
if isinstance(pre_add, Model):
|
|
1706
|
+
item = pre_add
|
|
1707
|
+
else:
|
|
1708
|
+
before_commit_runner.remove_tasks_by_tag("file")
|
|
1709
|
+
after_commit_runner.remove_tasks_by_tag("file")
|
|
1710
|
+
return pre_add
|
|
1706
1711
|
item = await smart_run(self.datamodel.add, session, item)
|
|
1712
|
+
after_commit_runner.remove_tasks_by_tag(
|
|
1713
|
+
"file"
|
|
1714
|
+
) # Delete any file tasks scheduled to revert files on error
|
|
1707
1715
|
post_add = await smart_run(
|
|
1708
1716
|
self.post_add,
|
|
1709
1717
|
item,
|
|
@@ -1739,50 +1747,57 @@ class ModelRestApi(BaseApi):
|
|
|
1739
1747
|
If you are overriding this method, make sure to copy all the decorators too.
|
|
1740
1748
|
"""
|
|
1741
1749
|
async with AsyncTaskRunner():
|
|
1742
|
-
async with AsyncTaskRunner(
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1750
|
+
async with AsyncTaskRunner(
|
|
1751
|
+
"after_commit", run_tasks_even_if_exception=True
|
|
1752
|
+
) as after_commit_runner:
|
|
1753
|
+
async with AsyncTaskRunner("before_commit") as before_commit_runner:
|
|
1754
|
+
item = await smart_run(
|
|
1755
|
+
self.datamodel.get_one,
|
|
1756
|
+
session,
|
|
1757
|
+
params={
|
|
1758
|
+
"list_columns": self.show_select_columns,
|
|
1759
|
+
"where_id": id,
|
|
1760
|
+
"filter_classes": self.base_filters,
|
|
1761
|
+
"opr_filter_classes": self.base_opr_filters,
|
|
1762
|
+
},
|
|
1756
1763
|
)
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1764
|
+
if not item:
|
|
1765
|
+
raise HTTPException(
|
|
1766
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1767
|
+
)
|
|
1768
|
+
body_json = await smart_run(
|
|
1769
|
+
self._process_body,
|
|
1770
|
+
session,
|
|
1771
|
+
body,
|
|
1772
|
+
self.edit_query_rel_fields,
|
|
1773
|
+
self.edit_schema_extra_fields.keys()
|
|
1774
|
+
if self.edit_schema_extra_fields
|
|
1775
|
+
else None,
|
|
1776
|
+
item=item,
|
|
1777
|
+
)
|
|
1778
|
+
await smart_run(
|
|
1779
|
+
self.pre_update_merge,
|
|
1780
|
+
item,
|
|
1781
|
+
body_json,
|
|
1782
|
+
PARAM_BODY_SESSION(body=body, session=session),
|
|
1783
|
+
)
|
|
1784
|
+
item.update(body_json)
|
|
1785
|
+
pre_update = await smart_run(
|
|
1786
|
+
self.pre_update,
|
|
1787
|
+
item,
|
|
1788
|
+
PARAM_BODY_SESSION(body=body, session=session),
|
|
1789
|
+
)
|
|
1790
|
+
if pre_update is not None:
|
|
1791
|
+
if isinstance(pre_update, Model):
|
|
1792
|
+
item = pre_update
|
|
1793
|
+
else:
|
|
1794
|
+
before_commit_runner.remove_tasks_by_tag("file")
|
|
1795
|
+
after_commit_runner.remove_tasks_by_tag("file")
|
|
1796
|
+
return pre_update
|
|
1785
1797
|
item = await smart_run(self.datamodel.edit, session, item)
|
|
1798
|
+
after_commit_runner.remove_tasks_by_tag(
|
|
1799
|
+
"file"
|
|
1800
|
+
) # Delete any file tasks scheduled to revert files on error
|
|
1786
1801
|
post_update = await smart_run(
|
|
1787
1802
|
self.post_update,
|
|
1788
1803
|
item,
|
|
@@ -1817,60 +1832,75 @@ class ModelRestApi(BaseApi):
|
|
|
1817
1832
|
If you are overriding this method, make sure to copy all the decorators too.
|
|
1818
1833
|
"""
|
|
1819
1834
|
async with AsyncTaskRunner():
|
|
1820
|
-
async with AsyncTaskRunner(
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1835
|
+
async with AsyncTaskRunner(
|
|
1836
|
+
"after_commit", run_tasks_even_if_exception=True
|
|
1837
|
+
) as after_commit_runner:
|
|
1838
|
+
async with AsyncTaskRunner("before_commit") as before_commit_runner:
|
|
1839
|
+
item = await smart_run(
|
|
1840
|
+
self.datamodel.get_one,
|
|
1841
|
+
session,
|
|
1842
|
+
params={
|
|
1843
|
+
"list_columns": self.show_select_columns,
|
|
1844
|
+
"where_id": id,
|
|
1845
|
+
"filter_classes": self.base_filters,
|
|
1846
|
+
"opr_filter_classes": self.base_opr_filters,
|
|
1847
|
+
},
|
|
1834
1848
|
)
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
else:
|
|
1844
|
-
return pre_delete
|
|
1845
|
-
# Delete all related files and images
|
|
1846
|
-
file_and_image_columns = [
|
|
1847
|
-
x
|
|
1848
|
-
for x in self.datamodel.get_file_column_list()
|
|
1849
|
-
+ self.datamodel.get_image_column_list()
|
|
1850
|
-
]
|
|
1851
|
-
schema = self.datamodel.generate_schema(
|
|
1852
|
-
file_and_image_columns,
|
|
1853
|
-
with_id=False,
|
|
1854
|
-
with_name=False,
|
|
1855
|
-
with_property=False,
|
|
1856
|
-
)
|
|
1857
|
-
schema_data = schema.model_validate(item, from_attributes=True)
|
|
1858
|
-
for column in file_and_image_columns:
|
|
1859
|
-
fm = (
|
|
1860
|
-
self.datamodel.file_manager
|
|
1861
|
-
if self.datamodel.is_file(column)
|
|
1862
|
-
else self.datamodel.image_manager
|
|
1849
|
+
if not item:
|
|
1850
|
+
raise HTTPException(
|
|
1851
|
+
fastapi.status.HTTP_404_NOT_FOUND, ErrorCode.ITEM_NOT_FOUND
|
|
1852
|
+
)
|
|
1853
|
+
pre_delete = await smart_run(
|
|
1854
|
+
self.pre_delete,
|
|
1855
|
+
item,
|
|
1856
|
+
PARAM_ID_SESSION(id=id, session=session),
|
|
1863
1857
|
)
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1858
|
+
if pre_delete is not None:
|
|
1859
|
+
if isinstance(pre_delete, Model):
|
|
1860
|
+
item = pre_delete
|
|
1861
|
+
else:
|
|
1862
|
+
return pre_delete
|
|
1863
|
+
# Delete all related files and images
|
|
1864
|
+
file_and_image_columns = [
|
|
1865
|
+
x
|
|
1866
|
+
for x in self.datamodel.get_file_column_list()
|
|
1867
|
+
+ self.datamodel.get_image_column_list()
|
|
1868
|
+
]
|
|
1869
|
+
schema = self.datamodel.generate_schema(
|
|
1870
|
+
file_and_image_columns,
|
|
1871
|
+
with_id=False,
|
|
1872
|
+
with_name=False,
|
|
1873
|
+
with_property=False,
|
|
1874
|
+
)
|
|
1875
|
+
schema_data = schema.model_validate(item, from_attributes=True)
|
|
1876
|
+
for column in file_and_image_columns:
|
|
1877
|
+
fm = (
|
|
1878
|
+
self.datamodel.file_manager
|
|
1879
|
+
if self.datamodel.is_file(column)
|
|
1880
|
+
else self.datamodel.image_manager
|
|
1872
1881
|
)
|
|
1882
|
+
filenames = getattr(schema_data, column, None) or []
|
|
1883
|
+
if not isinstance(filenames, list):
|
|
1884
|
+
filenames = [filenames]
|
|
1885
|
+
for filename in filenames:
|
|
1886
|
+
old_content = await smart_run(fm.get_file, filename)
|
|
1887
|
+
before_commit_runner.add_task(
|
|
1888
|
+
lambda fm=fm, filename=filename: smart_run(
|
|
1889
|
+
fm.delete_file, filename
|
|
1890
|
+
)
|
|
1891
|
+
)
|
|
1892
|
+
after_commit_runner.add_task(
|
|
1893
|
+
lambda fm=fm,
|
|
1894
|
+
content=old_content,
|
|
1895
|
+
filename=filename: smart_run(
|
|
1896
|
+
fm.save_content_to_file, content, filename
|
|
1897
|
+
),
|
|
1898
|
+
tags=["file"],
|
|
1899
|
+
)
|
|
1873
1900
|
await smart_run(self.datamodel.delete, session, item)
|
|
1901
|
+
after_commit_runner.remove_tasks_by_tag(
|
|
1902
|
+
"file"
|
|
1903
|
+
) # Delete any file tasks scheduled to revert files on error
|
|
1874
1904
|
post_delete = await smart_run(
|
|
1875
1905
|
self.post_delete,
|
|
1876
1906
|
item,
|
|
@@ -2371,15 +2401,30 @@ class ModelRestApi(BaseApi):
|
|
|
2371
2401
|
)
|
|
2372
2402
|
if item and hasattr(item, key) and getattr(item, key):
|
|
2373
2403
|
actual_old_filenames = getattr(item, key)
|
|
2404
|
+
before_commit_runner = AsyncTaskRunner.get_runner(
|
|
2405
|
+
"before_commit"
|
|
2406
|
+
)
|
|
2407
|
+
after_commit_runner = AsyncTaskRunner.get_runner(
|
|
2408
|
+
"after_commit"
|
|
2409
|
+
)
|
|
2374
2410
|
# Delete only the files or images that are not in the new old_filenames
|
|
2375
2411
|
for filename in actual_old_filenames:
|
|
2376
2412
|
if filename not in old_filenames:
|
|
2377
|
-
|
|
2413
|
+
old_content = await smart_run(fm.get_file, filename)
|
|
2414
|
+
before_commit_runner.add_task(
|
|
2378
2415
|
lambda fm=fm, old_filename=filename: smart_run(
|
|
2379
2416
|
fm.delete_file, old_filename
|
|
2380
2417
|
),
|
|
2381
2418
|
tags=["file"],
|
|
2382
2419
|
)
|
|
2420
|
+
after_commit_runner.add_task(
|
|
2421
|
+
lambda fm=fm,
|
|
2422
|
+
content=old_content,
|
|
2423
|
+
filename=filename: smart_run(
|
|
2424
|
+
fm.save_content_to_file, content, filename
|
|
2425
|
+
),
|
|
2426
|
+
tags=["file"],
|
|
2427
|
+
)
|
|
2383
2428
|
|
|
2384
2429
|
new_filenames = []
|
|
2385
2430
|
# Loop through value instead of only file values so the order is maintained
|
|
@@ -2395,12 +2440,27 @@ class ModelRestApi(BaseApi):
|
|
|
2395
2440
|
# Delete existing file or image if it is being updated
|
|
2396
2441
|
if item and hasattr(item, key) and getattr(item, key):
|
|
2397
2442
|
filename = getattr(item, key)
|
|
2398
|
-
|
|
2443
|
+
old_content = await smart_run(fm.get_file, filename)
|
|
2444
|
+
before_commit_runner = AsyncTaskRunner.get_runner(
|
|
2445
|
+
"before_commit"
|
|
2446
|
+
)
|
|
2447
|
+
after_commit_runner = AsyncTaskRunner.get_runner(
|
|
2448
|
+
"after_commit"
|
|
2449
|
+
)
|
|
2450
|
+
before_commit_runner.add_task(
|
|
2399
2451
|
lambda fm=fm, old_filename=filename: smart_run(
|
|
2400
2452
|
fm.delete_file, old_filename
|
|
2401
2453
|
),
|
|
2402
2454
|
tags=["file"],
|
|
2403
2455
|
)
|
|
2456
|
+
after_commit_runner.add_task(
|
|
2457
|
+
lambda fm=fm,
|
|
2458
|
+
content=old_content,
|
|
2459
|
+
filename=filename: smart_run(
|
|
2460
|
+
fm.save_content_to_file, content, filename
|
|
2461
|
+
),
|
|
2462
|
+
tags=["file"],
|
|
2463
|
+
)
|
|
2404
2464
|
|
|
2405
2465
|
# Only process if the value exists and is not None
|
|
2406
2466
|
if value:
|
|
@@ -2501,12 +2561,30 @@ class ModelRestApi(BaseApi):
|
|
|
2501
2561
|
),
|
|
2502
2562
|
)
|
|
2503
2563
|
content = await file.read()
|
|
2504
|
-
|
|
2564
|
+
if not fm.is_file_size_allowed(content):
|
|
2565
|
+
raise HTTPWithValidationException(
|
|
2566
|
+
fastapi.status.HTTP_400_BAD_REQUEST,
|
|
2567
|
+
"bytes_too_long",
|
|
2568
|
+
"body",
|
|
2569
|
+
key,
|
|
2570
|
+
translate(
|
|
2571
|
+
"File size from '{filename}' exceeds the allowed limit {file_size_limit}.",
|
|
2572
|
+
filename=file.filename,
|
|
2573
|
+
file_size_limit=format_file_size(fm.max_file_size),
|
|
2574
|
+
),
|
|
2575
|
+
)
|
|
2576
|
+
before_commit_runner = AsyncTaskRunner.get_runner("before_commit")
|
|
2577
|
+
after_commit_runner = AsyncTaskRunner.get_runner("after_commit")
|
|
2578
|
+
before_commit_runner.add_task(
|
|
2505
2579
|
lambda fm=fm, content=content, new_name=new_name: smart_run(
|
|
2506
2580
|
fm.save_content_to_file, content, new_name
|
|
2507
2581
|
),
|
|
2508
2582
|
tags=["file"],
|
|
2509
2583
|
)
|
|
2584
|
+
after_commit_runner.add_task(
|
|
2585
|
+
lambda fm=fm, new_name=new_name: smart_run(fm.delete_file, new_name),
|
|
2586
|
+
tags=["file"],
|
|
2587
|
+
)
|
|
2510
2588
|
return new_name
|
|
2511
2589
|
|
|
2512
2590
|
"""
|
fastapi_rtk/bases/__init__.py
CHANGED
|
@@ -6,10 +6,22 @@ import fastapi
|
|
|
6
6
|
from ..exceptions import FastAPIReactToolkitException
|
|
7
7
|
from ..utils import hooks, lazy, uuid_namegen
|
|
8
8
|
|
|
9
|
-
__all__ = [
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseFileException",
|
|
11
|
+
"FileNotAllowedException",
|
|
12
|
+
"FileTooLargeException",
|
|
13
|
+
"AbstractFileManager",
|
|
14
|
+
"AbstractImageManager",
|
|
15
|
+
]
|
|
10
16
|
|
|
11
17
|
|
|
12
|
-
class
|
|
18
|
+
class BaseFileException(FastAPIReactToolkitException):
|
|
19
|
+
"""
|
|
20
|
+
Base exception class for file-related errors.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileNotAllowedException(BaseFileException):
|
|
13
25
|
"""
|
|
14
26
|
Exception raised when a file is not allowed based on its extension.
|
|
15
27
|
"""
|
|
@@ -18,13 +30,25 @@ class FileNotAllowedException(FastAPIReactToolkitException):
|
|
|
18
30
|
super().__init__(f"File '{filename}' is not allowed.")
|
|
19
31
|
|
|
20
32
|
|
|
33
|
+
class FileTooLargeException(BaseFileException):
|
|
34
|
+
"""
|
|
35
|
+
Exception raised when a file exceeds the maximum allowed size.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, filename: str, max_size: int):
|
|
39
|
+
super().__init__(
|
|
40
|
+
f"File '{filename}' exceeds the maximum allowed size of {max_size} bytes."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
21
44
|
class AbstractFileManager(abc.ABC):
|
|
22
45
|
"""
|
|
23
46
|
Abstract base class for file managers.
|
|
24
47
|
"""
|
|
25
48
|
|
|
26
49
|
base_path: str = None
|
|
27
|
-
allowed_extensions: list[str] = None
|
|
50
|
+
allowed_extensions: list[str] | None = None
|
|
51
|
+
max_file_size: int | None = None
|
|
28
52
|
namegen = lazy(lambda: uuid_namegen)
|
|
29
53
|
permission = lazy(lambda: 0o755)
|
|
30
54
|
|
|
@@ -32,6 +56,7 @@ class AbstractFileManager(abc.ABC):
|
|
|
32
56
|
self,
|
|
33
57
|
base_path: str | None = None,
|
|
34
58
|
allowed_extensions: list[str] | None = None,
|
|
59
|
+
max_file_size: int | None = None,
|
|
35
60
|
namegen: typing.Callable[[str], str] | None = None,
|
|
36
61
|
permission: int | None = None,
|
|
37
62
|
):
|
|
@@ -41,6 +66,7 @@ class AbstractFileManager(abc.ABC):
|
|
|
41
66
|
Args:
|
|
42
67
|
base_path (str | None, optional): Base path for file storage. Defaults to None.
|
|
43
68
|
allowed_extensions (list[str] | None, optional): Allowed file extensions. Defaults to None.
|
|
69
|
+
max_file_size (int | None, optional): Maximum file size allowed. Defaults to None.
|
|
44
70
|
namegen (typing.Callable[[str], str] | None, optional): Callable for generating file names. Defaults to None.
|
|
45
71
|
permission (int | None, optional): File permission settings. Defaults to None.
|
|
46
72
|
|
|
@@ -51,6 +77,8 @@ class AbstractFileManager(abc.ABC):
|
|
|
51
77
|
self.base_path = base_path
|
|
52
78
|
if allowed_extensions is not None:
|
|
53
79
|
self.allowed_extensions = allowed_extensions
|
|
80
|
+
if max_file_size is not None:
|
|
81
|
+
self.max_file_size = max_file_size
|
|
54
82
|
if namegen is not None:
|
|
55
83
|
self.namegen = namegen
|
|
56
84
|
if permission is not None:
|
|
@@ -66,15 +94,26 @@ class AbstractFileManager(abc.ABC):
|
|
|
66
94
|
|
|
67
95
|
def __init_subclass__(cls):
|
|
68
96
|
# Add pre-hook to save_file and save_content_to_file to check if the file is allowed
|
|
69
|
-
def check_is_file_allowed(self, *args, **kwargs):
|
|
70
|
-
filename = None
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
filename = args[1]
|
|
75
|
-
if filename and not self.is_filename_allowed(filename):
|
|
97
|
+
def check_is_file_allowed(self: typing.Self, *args, **kwargs):
|
|
98
|
+
filename = kwargs.get("filename", args[1] if len(args) > 1 else None)
|
|
99
|
+
if not filename:
|
|
100
|
+
raise ValueError("Filename must be provided.")
|
|
101
|
+
if not self.is_filename_allowed(filename):
|
|
76
102
|
raise FileNotAllowedException(filename)
|
|
77
103
|
|
|
104
|
+
# Add pre-hook to save_file and save_content_to_file to check if the file size is allowed
|
|
105
|
+
def check_is_file_size_allowed(self: typing.Self, *args, **kwargs):
|
|
106
|
+
input = kwargs.get(
|
|
107
|
+
"file_data", args[0] if len(args) > 0 else None
|
|
108
|
+
) or kwargs.get("content", args[0] if len(args) > 0 else None)
|
|
109
|
+
filename = kwargs.get("filename", args[1] if len(args) > 1 else None)
|
|
110
|
+
if not input or not filename:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Both filename and file data/content must be provided."
|
|
113
|
+
)
|
|
114
|
+
if not self.is_file_size_allowed(input):
|
|
115
|
+
raise FileTooLargeException(filename, self.max_file_size)
|
|
116
|
+
|
|
78
117
|
if cls.save_file is not AbstractFileManager.save_file:
|
|
79
118
|
wrapped_save_file = hooks(pre=check_is_file_allowed)(cls.save_file)
|
|
80
119
|
cls.save_file = wrapped_save_file
|
|
@@ -83,6 +122,16 @@ class AbstractFileManager(abc.ABC):
|
|
|
83
122
|
cls.save_content_to_file
|
|
84
123
|
)
|
|
85
124
|
cls.save_content_to_file = wrapped_save_content_to_file
|
|
125
|
+
if cls.save_file is not AbstractFileManager.save_file:
|
|
126
|
+
wrapped_save_file_size = hooks(pre=check_is_file_size_allowed)(
|
|
127
|
+
cls.save_file
|
|
128
|
+
)
|
|
129
|
+
cls.save_file = wrapped_save_file_size
|
|
130
|
+
if cls.save_content_to_file is not AbstractFileManager.save_content_to_file:
|
|
131
|
+
wrapped_save_content_size = hooks(pre=check_is_file_size_allowed)(
|
|
132
|
+
cls.save_content_to_file
|
|
133
|
+
)
|
|
134
|
+
cls.save_content_to_file = wrapped_save_content_size
|
|
86
135
|
|
|
87
136
|
"""
|
|
88
137
|
--------------------------------------------------------------------------------------------------------
|
|
@@ -243,6 +292,29 @@ class AbstractFileManager(abc.ABC):
|
|
|
243
292
|
and filename.rsplit(".", 1)[1].lower() in self.allowed_extensions
|
|
244
293
|
)
|
|
245
294
|
|
|
295
|
+
def is_file_size_allowed(self, file_size: int | fastapi.UploadFile | bytes | str):
|
|
296
|
+
"""
|
|
297
|
+
Check if a file size is allowed based on the maximum file size.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
file_size (int | fastapi.UploadFile | bytes | str): The size of the file in bytes or the file data.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
bool: True if the file size is allowed, False otherwise.
|
|
304
|
+
"""
|
|
305
|
+
if self.max_file_size is None:
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
size = 0
|
|
309
|
+
if isinstance(file_size, int):
|
|
310
|
+
size = file_size
|
|
311
|
+
elif isinstance(file_size, fastapi.UploadFile):
|
|
312
|
+
size = file_size.size or 0
|
|
313
|
+
elif isinstance(file_size, (bytes, str)):
|
|
314
|
+
size = len(file_size)
|
|
315
|
+
|
|
316
|
+
return size <= self.max_file_size
|
|
317
|
+
|
|
246
318
|
def generate_name(self, filename: str) -> str:
|
|
247
319
|
"""
|
|
248
320
|
Generates a name for the given file data.
|
|
@@ -16,6 +16,7 @@ class FileManager(AbstractFileManager):
|
|
|
16
16
|
|
|
17
17
|
base_path = lazy(lambda: Setting.UPLOAD_FOLDER)
|
|
18
18
|
allowed_extensions = lazy(lambda: Setting.FILE_ALLOWED_EXTENSIONS)
|
|
19
|
+
max_file_size = lazy(lambda: Setting.FILE_MAX_SIZE)
|
|
19
20
|
|
|
20
21
|
def post_init(self):
|
|
21
22
|
if not self.base_path:
|
|
@@ -13,6 +13,7 @@ class ImageManager(FileManager, AbstractImageManager):
|
|
|
13
13
|
|
|
14
14
|
base_path = lazy(lambda: Setting.IMG_UPLOAD_FOLDER)
|
|
15
15
|
allowed_extensions = lazy(lambda: Setting.IMG_ALLOWED_EXTENSIONS)
|
|
16
|
+
max_file_size = lazy(lambda: Setting.IMG_MAX_SIZE)
|
|
16
17
|
|
|
17
18
|
def post_init(self):
|
|
18
19
|
if not self.base_path:
|