files
tit.server.routes.files ¶
/api/files/* — jailed file reads and bounded custom-mask imports (v1).
Every route resolves its path against the project directory (all catalog
routes hand back absolute container paths already) and against the bundled
resources/ tree (atlases, electrode caps -- read-only reference data a
viewer may legitimately want, e.g. the MNI152 template), and refuses to
serve anything that resolves outside both. No path rule beyond that jail
check lives here; content comes from tit.catalog (reports) or is read
directly for generic artifacts, matching the other v1 route modules.
raw ¶
raw(request: Request, path: str, range_header: str | None = Header(None, alias='Range'), if_none_match: str | None = Header(None, alias='If-None-Match'), if_range: str | None = Header(None, alias='If-Range')) -> Response
Stream one project file to the in-app viewer as opaque bytes.
Unlike /api/files/artifact this is not restricted to the document
extension allow-list -- the viewer needs .nii.gz, .msh,
.msh.opt, .gii, *_LUT.txt, .lut and friends, none of
which that route will serve. The trade is the opposite response policy:
every response is application/octet-stream with nosniff and
attachment, and the handful of extensions a browser could execute as
a document in this origin are refused outright, so nothing served here
can ever become a page.
The URL is the file's absolute path minus its leading slash
(/api/files/raw/mnt/000/.../T1.nii.gz) rather than a ?path=
query, because the engine's loader takes the file name, the gzip
decision and its volume-vs-mesh routing from the URL's last segment.
Range/206, If-Range and ETag/304 are supported so a large mesh
can be resumed. No response ever carries a Content-Encoding: a
.nii.gz must reach the viewer still deflated (it inflates the stream
itself), and an encoded body would break both Content-Length and
ranges.
This route is the whole reason viewing needs no X11 at all
(docs/dev/DECISIONS.md § 2026-09-03 (One Docker image and a real
development loop)): bytes are served to the app's own WebGL2 panes, or
named by an exported scene the native TetraVox app opens on the host --
never to an external Freeview/Gmsh process here.
Source code in tit/server/routes/files.py
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | |
upload_mask
async
¶
upload_mask(request: Request, name: Annotated[str, Query()], subject: Annotated[SubjectId, Query()]) -> dict
Store a validated mask under this subject; coordinate space is chosen per job.