Skip to content

Commit 0fc2a9e

Browse files
committed
Find just one file over limit in transaction
1 parent 50b6224 commit 0fc2a9e

File tree

5 files changed

+94
-43
lines changed

5 files changed

+94
-43
lines changed

mergin/client_push.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -486,14 +486,17 @@ def get_push_changes_batch(mc, mp: MerginProject) -> Tuple[LocalChanges, int]:
486486
updated=[LocalChange(**change) for change in changes["updated"]],
487487
removed=[LocalChange(**change) for change in changes["removed"]],
488488
)
489-
if local_changes.get_media_upload_size() > MAX_UPLOAD_MEDIA_SIZE:
489+
490+
over_limit_media = local_changes.get_media_upload_over_size(MAX_UPLOAD_MEDIA_SIZE)
491+
if over_limit_media:
490492
raise ClientError(
491-
f"Total size of media files to upload exceeds the maximum allowed size of {MAX_UPLOAD_MEDIA_SIZE / (1024**3)} GiB."
493+
f"File {over_limit_media.path} to upload exceeds the maximum allowed size of {MAX_UPLOAD_MEDIA_SIZE / (1024**3)} GB."
492494
)
493495

494-
if local_changes.get_gpgk_upload_size() > MAX_UPLOAD_VERSIONED_SIZE:
496+
over_limit_gpkg = local_changes.get_gpgk_upload_over_size(MAX_UPLOAD_VERSIONED_SIZE)
497+
if over_limit_gpkg:
495498
raise ClientError(
496-
f"Total size of GPKG files to upload exceeds the maximum allowed size of {MAX_UPLOAD_VERSIONED_SIZE / (1024**3)} GiB."
499+
f"Geopackage {over_limit_gpkg.path} to upload exceeds the maximum allowed size of {MAX_UPLOAD_VERSIONED_SIZE / (1024**3)} GB."
497500
)
498501

499502
return local_changes, sum(len(v) for v in changes.values())

mergin/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
# seconds to wait between sync callback calls
2525
SYNC_CALLBACK_WAIT = 0.01
2626

27-
# maximum size of media files able to upload in one push (in bytes)
27+
# maximum size of media file able to upload in one push (in bytes)
2828
MAX_UPLOAD_MEDIA_SIZE = 10 * (1024**3)
2929

30-
# maximum size of GPKG files able to upload in one push (in bytes)
30+
# maximum size of GPKG file able to upload in one push (in bytes)
3131
MAX_UPLOAD_VERSIONED_SIZE = 5 * (1024**3)
3232

3333
# default URL for submitting logs

mergin/local_changes.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,22 @@ def update_chunks(self, server_chunks: List[Tuple[str, str]]) -> None:
113113
for change in self.updated:
114114
change.chunks = self._map_unique_chunks(change.chunks, server_chunks)
115115

116-
def get_media_upload_size(self) -> int:
116+
def get_media_upload_over_size(self, size_limit: int) -> Optional[LocalChange]:
117117
"""
118-
Calculate the total size of media files in added and updated changes.
118+
Find the first media file in added and updated changes that exceeds the size limit.
119+
:return: The first LocalChange that exceeds the size limit, or None if no such file exists.
119120
"""
120-
total_size = 0
121121
for change in self.get_upload_changes():
122-
if not is_versioned_file(change.path):
123-
total_size += change.size
124-
return total_size
122+
if not is_versioned_file(change.path) and change.size > size_limit:
123+
return change
125124

126-
def get_gpgk_upload_size(self) -> int:
125+
def get_gpgk_upload_over_size(self, size_limit: int) -> Optional[LocalChange]:
127126
"""
128-
Calculate the total size of gpgk files in added and updated changes.
129-
Do not calculate diffs (only new or overwriten files).
127+
Find the first GPKG file in added and updated changes that exceeds the size limit.
128+
Do not include diffs (only new or overwritten files).
129+
:param size_limit: The size limit in bytes.
130+
:return: The first LocalChange that exceeds the size limit, or None if no such file exists.
130131
"""
131-
total_size = 0
132132
for change in self.get_upload_changes():
133-
if is_versioned_file(change.path) and not change.diff:
134-
total_size += change.size
135-
return total_size
133+
if is_versioned_file(change.path) and not change.diff and change.size > size_limit:
134+
return change

mergin/test/test_client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3211,3 +3211,21 @@ def test_client_project_sync_retry(mc):
32113211
with pytest.raises(ClientError):
32123212
mc.sync_project(project_dir)
32133213
assert mock_push_project_async.call_count == 2
3214+
3215+
def test_push_file_limits(mc):
3216+
test_project = "test_push_file_limits"
3217+
project = API_USER + "/" + test_project
3218+
project_dir = os.path.join(TMP_DIR, test_project)
3219+
cleanup(mc, project, [project_dir])
3220+
mc.create_project(test_project)
3221+
mc.download_project(project, project_dir)
3222+
shutil.copy(os.path.join(TEST_DATA_DIR, "base.gpkg"), project_dir)
3223+
# setting to some minimal value to mock limit hit
3224+
with patch("mergin.client_push.MAX_UPLOAD_VERSIONED_SIZE", 1):
3225+
with pytest.raises(ClientError, match=f"base.gpkg to upload exceeds the maximum allowed size of {1/1024**3}"):
3226+
mc.push_project(project_dir)
3227+
3228+
shutil.copy(os.path.join(TEST_DATA_DIR, "test.txt"), project_dir)
3229+
with patch("mergin.client_push.MAX_UPLOAD_MEDIA_SIZE", 1):
3230+
with pytest.raises(ClientError, match=f"test.txt to upload exceeds the maximum allowed size of {1/1024**3}"):
3231+
mc.push_project(project_dir)

mergin/test/test_local_changes.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22

3-
from ..local_changes import LocalChange, LocalChanges
3+
from ..local_changes import LocalChange, LocalChanges, MAX_UPLOAD_CHANGES
44

55

66
def test_local_changes_from_dict():
@@ -120,60 +120,91 @@ def test_local_changes_get_upload_changes():
120120
assert upload_changes[1].path == "file2.txt" # Second change is from updated
121121

122122

123-
def test_local_changes_get_media_upload_size():
124-
"""Test the get_media_upload_size method of LocalChanges."""
123+
def test_local_changes_get_media_upload_over_size():
124+
"""Test the get_media_upload_file method of LocalChanges."""
125+
# Define constants
126+
SIZE_LIMIT_MB = 10
127+
SIZE_LIMIT_BYTES = SIZE_LIMIT_MB * 1024 * 1024
128+
SMALL_FILE_SIZE = 1024
129+
LARGE_FILE_SIZE = 15 * 1024 * 1024
130+
125131
# Create sample LocalChange instances
126132
added = [
127-
LocalChange(path="file1.txt", checksum="abc123", size=1024, mtime=datetime.now()),
128-
LocalChange(path="file2.jpg", checksum="xyz789", size=2048, mtime=datetime.now()),
133+
LocalChange(path="file1.txt", checksum="abc123", size=SMALL_FILE_SIZE, mtime=datetime.now()),
134+
LocalChange(path="file2.jpg", checksum="xyz789", size=LARGE_FILE_SIZE, mtime=datetime.now()), # Over limit
129135
]
130136
updated = [
131-
LocalChange(path="file3.mp4", checksum="lmn456", size=5120, mtime=datetime.now()),
132-
LocalChange(path="file4.gpkg", checksum="opq123", size=1024, mtime=datetime.now()),
137+
LocalChange(path="file3.mp4", checksum="lmn456", size=5 * 1024 * 1024, mtime=datetime.now()),
138+
LocalChange(path="file4.gpkg", checksum="opq123", size=SMALL_FILE_SIZE, mtime=datetime.now()),
133139
]
134140

135141
# Initialize LocalChanges
136142
local_changes = LocalChanges(added=added, updated=updated)
137143

138-
# Call get_media_upload_size
139-
media_size = local_changes.get_media_upload_size()
144+
# Call get_media_upload_file with a size limit
145+
media_file = local_changes.get_media_upload_over_size(SIZE_LIMIT_BYTES)
140146

141147
# Assertions
142-
assert media_size == 8192 # Only non-versioned files (txt, jpg, mp4) are included
148+
assert media_file is not None
149+
assert media_file.path == "file2.jpg" # The first file over the limit
150+
assert media_file.size == LARGE_FILE_SIZE
151+
143152

153+
def test_local_changes_get_gpgk_upload_over_size():
154+
"""Test the get_gpgk_upload_file method of LocalChanges."""
155+
# Define constants
156+
SIZE_LIMIT_MB = 10
157+
SIZE_LIMIT_BYTES = SIZE_LIMIT_MB * 1024 * 1024
158+
SMALL_FILE_SIZE = 1024
159+
LARGE_FILE_SIZE = 15 * 1024 * 1024
144160

145-
def test_local_changes_get_gpgk_upload_size():
146-
"""Test the get_gpgk_upload_size method of LocalChanges."""
147161
# Create sample LocalChange instances
148162
added = [
149-
LocalChange(path="file1.gpkg", checksum="abc123", size=1024, mtime=datetime.now()),
150-
LocalChange(path="file2.gpkg", checksum="xyz789", size=2048, mtime=datetime.now(), diff={"path": "diff1"}),
163+
LocalChange(path="file1.gpkg", checksum="abc123", size=SMALL_FILE_SIZE, mtime=datetime.now()),
164+
LocalChange(
165+
path="file2.gpkg", checksum="xyz789", size=LARGE_FILE_SIZE, mtime=datetime.now(), diff=None
166+
), # Over limit
151167
]
152168
updated = [
153-
LocalChange(path="file3.gpkg", checksum="lmn456", size=5120, mtime=datetime.now()),
154-
LocalChange(path="file4.txt", checksum="opq123", size=1024, mtime=datetime.now()),
169+
LocalChange(path="file3.gpkg", checksum="lmn456", size=5 * 1024 * 1024, mtime=datetime.now()),
170+
LocalChange(path="file4.txt", checksum="opq123", size=SMALL_FILE_SIZE, mtime=datetime.now()),
155171
]
156172

157173
# Initialize LocalChanges
158174
local_changes = LocalChanges(added=added, updated=updated)
159175

160-
# Call get_gpgk_upload_size
161-
gpkg_size = local_changes.get_gpgk_upload_size()
176+
# Call get_gpgk_upload_file with a size limit
177+
gpkg_file = local_changes.get_gpgk_upload_over_size(SIZE_LIMIT_BYTES)
162178

163179
# Assertions
164-
assert gpkg_size == 6144 # Only GPKG files without diffs are included
180+
assert gpkg_file is not None
181+
assert gpkg_file.path == "file2.gpkg" # The first GPKG file over the limit
182+
assert gpkg_file.size == LARGE_FILE_SIZE
183+
assert gpkg_file.diff is None # Ensure it doesn't include diffs
165184

166185

167186
def test_local_changes_post_init():
168187
"""Test the __post_init__ method of LocalChanges."""
188+
# Define constants
189+
ADDED_COUNT = 80
190+
UPDATED_COUNT = 21
191+
SMALL_FILE_SIZE = 1024
192+
LARGE_FILE_SIZE = 2048
193+
169194
# Create more than MAX_UPLOAD_CHANGES changes
170-
added = [LocalChange(path=f"file{i}.txt", checksum="abc123", size=1024, mtime=datetime.now()) for i in range(80)]
171-
updated = [LocalChange(path=f"file{i}.txt", checksum="xyz789", size=2048, mtime=datetime.now()) for i in range(21)]
195+
added = [
196+
LocalChange(path=f"file{i}.txt", checksum="abc123", size=SMALL_FILE_SIZE, mtime=datetime.now())
197+
for i in range(ADDED_COUNT)
198+
]
199+
updated = [
200+
LocalChange(path=f"file{i}.txt", checksum="xyz789", size=LARGE_FILE_SIZE, mtime=datetime.now())
201+
for i in range(UPDATED_COUNT)
202+
]
172203

173204
# Initialize LocalChanges
174205
local_changes = LocalChanges(added=added, updated=updated)
175206

176207
# Assertions
177-
assert len(local_changes.added) == 80 # All 80 added changes are included
178-
assert len(local_changes.updated) == 20 # Only 20 updated changes are included to respect the limit
179-
assert len(local_changes.added) + len(local_changes.updated) == 100 # Total is limited to MAX_UPLOAD_CHANGES
208+
assert len(local_changes.added) == ADDED_COUNT # All added changes are included
209+
assert len(local_changes.updated) == MAX_UPLOAD_CHANGES - ADDED_COUNT # Only enough updated changes are included
210+
assert len(local_changes.added) + len(local_changes.updated) == MAX_UPLOAD_CHANGES # Total is limited

0 commit comments

Comments
 (0)