Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0f0673c
Add keys to the MachineConfig model to control the 'rsync' and 'mkdir…
tieneupin Apr 21, 2026
f7e39b5
Create attribute in RSyncer to store 'chmod' permissions value, and p…
tieneupin Apr 21, 2026
df8d58f
Use folder permissions logic from the machine config for functions in…
tieneupin Apr 21, 2026
9f48b27
Add basic model validator to convert 'mkdir_chmod' string into an int
tieneupin Apr 21, 2026
6b26f4b
Added logic to backend functions to change folder permissions wheneve…
tieneupin Apr 28, 2026
b983bb6
Make octal int check stricter
tieneupin Apr 28, 2026
630ed6e
Fixed broken test
tieneupin Apr 28, 2026
599aff2
Fixed broken test
tieneupin Apr 28, 2026
1eded36
Add logic to 'prepare_gain' function to configure directory permissions
tieneupin Apr 28, 2026
3fa5a0c
Stricter default chmod permissions
tieneupin Apr 28, 2026
9de6721
Add test stubs for some functions in 'murfey.server.api.file_io_instr…
tieneupin Apr 28, 2026
56a511d
Added comment to describe the type of data the 'base_path' field in '…
tieneupin Apr 28, 2026
dc6d773
Added unit test for 'suggest_path'
tieneupin Apr 28, 2026
0bb34c9
Path constructed wrongly when 'extra_dir' is an empty string
tieneupin Apr 28, 2026
f97edfa
Add unit test for 'make_rsyncer_destination'
tieneupin Apr 28, 2026
03d2188
Use the actual MachineConfig instead of a MagicMock object
tieneupin Apr 28, 2026
dbb0ba4
Add test stubs for gain reference functions
tieneupin Apr 28, 2026
b057779
Added unit test for 'prepare_gain' function
tieneupin Apr 28, 2026
754bc12
Encase 'chmod' command for 'make_rsyncer_destination' in try-except b…
tieneupin Apr 29, 2026
247521b
Adjust logic for changing permissions of folders when making rsyncer …
tieneupin Apr 29, 2026
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
1 change: 1 addition & 0 deletions src/murfey/client/multigrid_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ def _start_rsyncer(
stop_callback=self._rsyncer_stopped,
do_transfer=self.do_transfer,
remove_files=remove_files,
chmod=self._machine_config.get("rsync_chmod", "D0750,F0750"),
substrings_blacklist=self._machine_config.get(
"substrings_blacklist", {"directories": [], "files": []}
),
Expand Down
4 changes: 3 additions & 1 deletion src/murfey/client/rsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def __init__(
local: bool = False,
do_transfer: bool = True,
remove_files: bool = False,
chmod: str = "D0750,F0750",
required_substrings_for_removal: List[str] = [],
substrings_blacklist: dict[str, list[str]] = {},
notify: bool = True,
Expand All @@ -75,6 +76,7 @@ def __init__(
self._local = local
self._do_transfer = do_transfer
self._remove_files = remove_files
self._chmod = chmod
self._required_substrings_for_removal = required_substrings_for_removal
self._substrings_blacklist = substrings_blacklist
self._notify = notify
Expand Down Expand Up @@ -501,7 +503,7 @@ def parse_stderr(line: str):
# Needed as a pair to trigger permission modifications
# Ref: https://serverfault.com/a/796341
"-p",
"--chmod=D0750,F0750", # Use extended chmod format
f"--chmod={self._chmod}", # Set permissions for transferred files and folders
]
# Add file locations
rsync_cmd.extend([".", self._remote])
Expand Down
38 changes: 30 additions & 8 deletions src/murfey/server/api/file_io_instrument.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from datetime import datetime
from logging import getLogger
from pathlib import Path
Expand Down Expand Up @@ -33,7 +34,9 @@


class SuggestedPathParameters(BaseModel):
base_path: Path
base_path: (
Path # Partial Path starting from immediately after the rsync destination
)
touch: bool = False
extra_directory: str = ""

Expand Down Expand Up @@ -86,9 +89,12 @@ def suggest_path(
count = count + 1 if count else 2
check_path = check_path.parent / f"{check_path_name}{count}"
if params.touch:
check_path.mkdir(mode=0o750)
check_path.mkdir()
os.chmod(check_path, mode=machine_config.mkdir_chmod)
if params.extra_directory:
(check_path / secure_filename(params.extra_directory)).mkdir(mode=0o750)
extra_dir = check_path / secure_filename(params.extra_directory)
extra_dir.mkdir()
os.chmod(extra_dir, mode=machine_config.mkdir_chmod)
return {"suggested_path": check_path.relative_to(rsync_basepath)}


Expand All @@ -100,19 +106,35 @@ class Dest(BaseModel):
def make_rsyncer_destination(session_id: int, destination: Dest, db=murfey_db):
secure_path_parts = [secure_filename(p) for p in destination.destination.parts]
destination_path = "/".join(secure_path_parts)
instrument_name = (
db.exec(select(Session).where(Session.id == session_id)).one().instrument_name
)
session_entry = db.exec(select(Session).where(Session.id == session_id)).one()
instrument_name = session_entry.instrument_name
visit = session_entry.visit
machine_config = get_machine_config(instrument_name=instrument_name)[
instrument_name
]
if not machine_config:
raise ValueError("No machine configuration set when making rsyncer destination")

# Make the destination directory and all parents
full_destination_path = (
machine_config.rsync_basepath or Path("")
).resolve() / destination_path
for parent_path in full_destination_path.parents:
parent_path.mkdir(mode=0o750, exist_ok=True)
full_destination_path.mkdir(parents=True, exist_ok=True)

# Change permissions for every folder after the visit directory
try:
visit_index = full_destination_path.parts.index(visit)
except ValueError:
logger.error(f"Could not find directory level {visit!r} in destination path")
raise
current_path = full_destination_path.parents[-(visit_index + 1)]
for part in full_destination_path.parts[visit_index + 1 :]:
current_path = current_path / part
try:
os.chmod(current_path, mode=machine_config.mkdir_chmod)
except PermissionError:
logger.warning(f"Unable to change permissions for {current_path}")
continue
return destination


Expand Down
1 change: 1 addition & 0 deletions src/murfey/server/api/file_io_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async def process_gain(
env,
rescale=gain_reference_params.rescale,
tag=gain_reference_params.tag,
chmod=machine_config.mkdir_chmod,
)
if new_gain_ref and new_gain_ref_superres:
return {
Expand Down
11 changes: 8 additions & 3 deletions src/murfey/server/api/workflow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import os
from datetime import datetime
from logging import getLogger
from pathlib import Path
Expand Down Expand Up @@ -607,9 +608,9 @@ async def request_spa_preprocessing(
db.close()

if not mrc_out.parent.exists():
Path(secure_filename(str(mrc_out))).parent.mkdir(
parents=True, exist_ok=True
)
mrc_out_dir = Path(secure_filename(str(mrc_out))).parent
mrc_out_dir.mkdir(parents=True, exist_ok=True)
os.chmod(mrc_out_dir, mode=machine_config.mkdir_chmod)
recipe_name = machine_config.recipes.get(
"em-spa-preprocess", "em-spa-preprocess"
)
Expand Down Expand Up @@ -836,6 +837,7 @@ async def request_tomography_preprocessing(
murfey_ids = _murfey_id(appid, db, number=1, close=False)
if not mrc_out.parent.exists():
mrc_out.parent.mkdir(parents=True, exist_ok=True)
os.chmod(mrc_out.parent, mode=machine_config.mkdir_chmod)

session_processing_parameters = db.exec(
select(SessionProcessingParameters).where(
Expand Down Expand Up @@ -988,6 +990,7 @@ def register_completed_tilt_series(
)
if not stack_file.parent.exists():
stack_file.parent.mkdir(parents=True)
os.chmod(stack_file.parent, mode=machine_config.mkdir_chmod)
tilt_offset = midpoint([float(get_angle(t)) for t in tilts])
zocalo_message = {
"recipes": ["em-tomo-align"],
Expand Down Expand Up @@ -1222,8 +1225,10 @@ async def make_gif(
/ "processed"
)
output_dir.mkdir(exist_ok=True)
os.chmod(output_dir, mode=machine_config.mkdir_chmod)
output_dir = output_dir / secure_filename(gif_params.raw_directory)
output_dir.mkdir(exist_ok=True)
os.chmod(output_dir, mode=machine_config.mkdir_chmod)
output_path = output_dir / f"lamella_{gif_params.lamella_number}_milling.gif"

if Image is not None:
Expand Down
3 changes: 3 additions & 0 deletions src/murfey/server/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import logging
import math
import os
import subprocess
import time
from datetime import datetime
Expand Down Expand Up @@ -1402,6 +1403,7 @@ def _flush_tomography_preprocessing(message: dict, _db):
p = Path(f.mrc_out)
if not p.parent.exists():
p.parent.mkdir(parents=True)
os.chmod(p.parent, mode=machine_config.mkdir_chmod)
movie = db.Movie(
murfey_id=murfey_ids[0],
data_collection_id=detached_ids[1],
Expand Down Expand Up @@ -1876,6 +1878,7 @@ def feedback_callback(header: dict, message: dict, _db=murfey_db) -> None:
)
if not stack_file.parent.exists():
stack_file.parent.mkdir(parents=True)
os.chmod(stack_file.parent, machine_config.mkdir_chmod)
tilt_offset = midpoint([float(get_angle(t)) for t in tilts])
zocalo_message = {
"recipes": ["em-tomo-align"],
Expand Down
14 changes: 9 additions & 5 deletions src/murfey/server/gain.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
env: Dict[str, str],
rescale: bool = True,
tag: str = "",
chmod: int = 0o750,
) -> Tuple[Path | None, Path | None]:
if not all(executables.get(s) for s in ("dm2mrc", "clip", "newstack")):
logger.error("No executables were provided to prepare the gain reference with")
Expand All @@ -57,10 +58,10 @@
return gain_out, gain_out_superres if rescale else gain_out
for k, v in env.items():
os.environ[k] = v
if tag:
secure_path(gain_path.parent / f"gain_{tag}").mkdir(exist_ok=True)
else:
secure_path(gain_path.parent / "gain").mkdir(exist_ok=True)
gain_tag = f"gain_{tag}" if tag else "gain"
gain_dir = secure_path(gain_path.parent / gain_tag)
gain_dir.mkdir(exist_ok=True)
os.chmod(gain_dir, chmod)

Check failure

Code scanning / CodeQL

Overly permissive file permissions High

Overly permissive mask in chmod sets file to group readable.
Comment thread
d-j-hatton marked this conversation as resolved.
Dismissed
gain_path = _sanitise(gain_path, tag)
flip = "flipx" if camera == Camera.K3_FLIPX else "flipy"
gain_path_mrc = gain_path.with_suffix(".mrc")
Expand Down Expand Up @@ -109,7 +110,10 @@


async def prepare_eer_gain(
gain_path: Path, executables: Dict[str, str], env: Dict[str, str], tag: str = ""
gain_path: Path,
executables: Dict[str, str],
env: Dict[str, str],
tag: str = "",
) -> Tuple[Path | None, Path | None]:
if not executables.get("tif2mrc"):
logger.error(
Expand Down
11 changes: 11 additions & 0 deletions src/murfey/util/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ class MachineConfig(BaseModel): # type: ignore
"directories": [],
"files": [],
}
mkdir_chmod: int = 0o750

# Rsync setup
rsync_url: str = ""
rsync_module: str = ""
rsync_basepath: Optional[Path] = None
rsync_chmod: str = "D0750,F0750"
allow_removal: bool = False

# Upstream data download setup
Expand Down Expand Up @@ -151,6 +153,15 @@ def validate_software_versions(cls, v: dict[str, Any]) -> dict[str, str]:
# Let it validate and fail as-is
return v

@field_validator("mkdir_chmod", mode="before")
@classmethod
def parse_octal_int(cls, value):
# Attempt to parse the string as an octal int
if isinstance(value, str) and value.startswith("0o") and value[2:].isdigit():
return int(value, 8)
# Return value as-is otherwise
return value


@lru_cache(maxsize=1)
def machine_config_from_file(
Expand Down
2 changes: 2 additions & 0 deletions src/murfey/workflows/spa/atlas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
from pathlib import Path

import mrcfile
Expand Down Expand Up @@ -37,6 +38,7 @@ def atlas_jpg_from_mrc(instrument_name: str, visit_name: str, atlas_mrc: Path):
/ secure_filename(f"{sample_id}_{atlas_mrc.stem}_fullres.jpg")
)
atlas_jpg_file.parent.mkdir(parents=True, exist_ok=True)
os.chmod(atlas_jpg_file.parent, mode=machine_config.mkdir_chmod)

data = data - data.min()
data = data.astype(float) * 255 / data.max()
Expand Down
10 changes: 8 additions & 2 deletions src/murfey/workflows/spa/flush_spa_preprocess.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
from pathlib import Path
from typing import Optional

Expand Down Expand Up @@ -164,11 +165,15 @@ def register_grid_square(
".tiff"
).is_file():
secured_grid_square_image_path_full_res = (
secured_grid_square_image_path_full_res.with_suffix(".tiff")
secured_grid_square_image_path_full_res.with_suffix(
".tiff"
)
)
else:
secured_grid_square_image_path_full_res = (
secured_grid_square_image_path_full_res.with_suffix(".mrc")
secured_grid_square_image_path_full_res.with_suffix(
".mrc"
)
)
smartem_client = SmartEMAPIClient(
base_url=machine_config.smartem_api_url, logger=logger
Expand Down Expand Up @@ -543,6 +548,7 @@ def flush_spa_preprocess(message: dict, murfey_db: Session) -> dict[str, bool]:
ppath = Path(f.file_path)
if not mrcp.parent.exists():
mrcp.parent.mkdir(parents=True)
os.chmod(mrcp.parent, mode=machine_config.mkdir_chmod)
movie = Movie(
murfey_id=murfey_ids[2 * i],
data_collection_id=collected_ids[1].id,
Expand Down
Loading
Loading