Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions flask-server/services/user_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,22 @@ def _write_sublabels(combined_labels_path: str, seg_dir: str, existing_seg_dir:
return written


def _store_nifti_gz(src: str, dst: str) -> None:
"""Store a NIfTI at dst as a genuinely gzip-compressed .nii.gz. If src is
already gzip (the usual .nii.gz upload) its bytes are preserved exactly; a raw
.nii is gzipped on the way in. Without this, copying an uncompressed upload to
a .nii.gz name would waste space AND write a file nibabel can't load (it
gunzips by extension)."""
with open(src, "rb") as f:
is_gzip = f.read(2) == b"\x1f\x8b"
if is_gzip:
shutil.copy2(src, dst)
else:
import gzip
with open(src, "rb") as fin, gzip.open(dst, "wb") as fout:
shutil.copyfileobj(fin, fout)


def _promote(root: str, case_id: str, ct_path: str, combined_labels_path: str,
existing_seg_dir: Optional[str], metadata: dict) -> None:
"""Write all of a case's files into `.partial` staging dirs, then publish each
Expand All @@ -327,8 +343,8 @@ def _promote(root: str, case_id: str, ct_path: str, combined_labels_path: str,
try:
os.makedirs(img_stage, exist_ok=True)
os.makedirs(mask_stage, exist_ok=True)
shutil.copy2(ct_path, os.path.join(img_stage, "ct.nii.gz"))
shutil.copy2(combined_labels_path, os.path.join(mask_stage, "combined_labels.nii.gz"))
_store_nifti_gz(ct_path, os.path.join(img_stage, "ct.nii.gz"))
_store_nifti_gz(combined_labels_path, os.path.join(mask_stage, "combined_labels.nii.gz"))
organs = _write_sublabels(combined_labels_path, os.path.join(mask_stage, "segmentations"),
existing_seg_dir)
meta = {**metadata, "case_id": case_id, "organs": organs}
Expand Down
25 changes: 25 additions & 0 deletions flask-server/tests/unit/test_user_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,31 @@ def test_voxel_guard_blocks_oom(ud, tmp_path, monkeypatch):
assert not ok and reason.startswith("too_many_voxels")


def test_uncompressed_upload_stored_as_gzip(ud, tmp_path):
"""A raw .nii upload must be gzipped on store, not copied under a .nii.gz name
(which would be uncompressed and unloadable)."""
np = pytest.importorskip("numpy")
nib = pytest.importorskip("nibabel")
vol = np.full((64, 64, 64), -1000.0, dtype="float32")
vol[20:40, 20:40, 20:40] = 60.0
vol[30:34, 30:34, 30:34] = 400.0
ct = str(tmp_path / "scan.nii") # RAW, uncompressed upload
nib.save(nib.Nifti1Image(vol, np.eye(4)), ct)
with open(ct, "rb") as f:
assert f.read(2) != b"\x1f\x8b" # confirm the source really is raw

out = tmp_path / "out"; out.mkdir()
mask = np.zeros((64, 64, 64), "uint8"); mask[10:30, 10:30, 10:30] = 14; mask[35:45, 35:45, 35:45] = 17
_write_nifti(str(out / "combined_labels.nii.gz"), mask)

ud._admit_and_store(ct, str(out), "ePAI", "u1", "1.2.3.4", "s")
stored = os.path.join(ud._root(), "image_only", "USER_00000001", "ct.nii.gz")
assert os.path.exists(stored)
with open(stored, "rb") as f:
assert f.read(2) == b"\x1f\x8b" # stored file is genuinely gzip
assert np.asarray(nib.load(stored).dataobj).shape == (64, 64, 64) # and loads back


def test_rejects_non_finite(ud, tmp_path):
np = pytest.importorskip("numpy")
pytest.importorskip("nibabel")
Expand Down
Loading