Files and the network
How a Plugin reads Project files, refers to resources, uses signed URLs and local files, and what differs between desktop and web.
The same Plugin runs in the desktop app, in a browser against a hosted Project, and against a ProjectServer on another machine. These rules keep it working in all three.
Five rules#
- Name Project files by Project path, such as
data/field.npz: forward slashes, relative to the Project root, nothing under.witwin/. Never send an OS path (C:\...) between the halves. - The Python half reads and writes Project files through
ctx.fs. It resolves the path against the open Project. A leading/means the Project root, anda/../bis read asb. It raisesProjectPathErrorfor anything else, with acode:invalid(not a string, or a NUL byte),absolute(a drive letter or UNC share),outside(..or a symlink that leaves the Project),runtime(under.witwin/) orroot(a read, write or delete of the Project root itself). Turn it intoRpcError("invalid_params", ...)in an RPC method. Studio's own Tools and imports check more strictly (traversal,hidden), so do not rely ona/../bin paths you hand to them. - The web half never builds a URL. It loads bytes through the
resourcesservice with aResourceRef. Nofetch(,new WebSocket,window.witwin,localhost,http://orgetActiveProjectHttpBasein a web half;npm run lint:architecturefails on them in system Plugins. - The user's own computer is not the server. A file the user picks or
drops comes in through
localFilesand is copied into the Project; the server never reads the user's disk. - Ask what the connection can do; never guess from the host name. Use
platform.has(...); the lint also fails onnavigator.userAgent-style sniffing andisDesktopin system Plugins.
localFiles, platform, fs and arrays are proposed services: a
third-party Plugin uses them only from a local path with the name in
enabledApiProposals (Web API).
Loading bytes in the web half: resources#
const resources = host.services.get('resources');
const response = await resources.fetch({ kind: 'projectFile', path: 'data/notes.txt' });
const { url, release } = await resources.objectUrl({ kind: 'projectFile', path: 'img/plot.png' });
const iframeSrc = await resources.signedUrl({ kind: 'pluginAsset', pluginId: 'witwin.example_notes', path: 'help.html' });ResourceRef | Points at |
|---|---|
{ kind: 'projectFile', path } | a file in the Project |
{ kind: 'pluginAsset', pluginId, path? } | a file in a Plugin's [web] folder |
{ kind: 'route', path, query? } | a server route. System Plugins only: a third-party Plugin gets an error and uses projectFile, pluginAsset or its own RPC methods. |
fetchadds the connection's authorization; use it for data.objectUrlgives ablob:URL for an<img>or a loader; callreleasewhen the element goes away.signedUrlgives a URL that loads without headers (an iframe, a loader that cannot set headers). It is short-lived; ask again rather than store it.
For small structured data, use RPC instead of a file route. Large arrays in
.npy / .npz files have a dedicated reader (arrays, proposed) that reads
a window without loading the whole file.
Writing Project files from the web half#
A third-party Plugin writes through its own RPC method, which calls
ctx.fs.write_text / write_bytes on the server. The proposed fs service
(list, read, write with expectedMtime, createFolder, createIn,
rename, move, copy, delete, reveal, openExternal,
onDidChange) serves the Project panel and Git; a third-party Plugin may use
it in development only, like every proposed service.
Files from the user's computer: localFiles#
const localFiles = host.services.get('localFiles');
const [file] = await localFiles.pick({ accept: '.csv' });
const projectPath = await file.importInto('data', { onConflict: 'keep_both' });pick opens the browser's file picker (also in the desktop app) and
resolves [] when the user cancels. fromFile wraps a File from a drop;
fromDroppedFolders keeps the folder structure. importInto(destDir, { onConflict }) copies the bytes into the Project and resolves the Project
path it wrote; onConflict is replace (default), skip or keep_both.
After that, use the Project path.
With nativeDialogs, showOpenDialog({ title, filters }) opens the
desktop's own dialog in the Project folder and resolves native paths (or
null). projectPath(nativePath) turns a path inside the Project into a
Project path; fromNativePath(path) wraps a picked path for importInto
and needs sameMachine. imports lists running imports with their progress
and lets you cancel or retry them.
Desktop versus web: platform#
| Capability | True when |
|---|---|
sameMachine | the ProjectServer runs on the user's computer (Local) |
nativeDialogs | the desktop app's native file dialogs are available |
revealLocal | "Show in Explorer/Finder" works |
serverProcess | the ProjectServer is a process the user runs (Local, Direct, Router), not a hosted sandbox |
pluginInstall | Plugins can be installed into this Project (false for hosted Projects) |
Hide a control whose capability is false; do not show it and fail. Commands
and menu rows can use the context key platform.<capability> in when.
Network in the Python half#
The Python half may call outside hosts. List them in
[permissions] network = ["overpass-api.de"]: the trust dialog shows the
list. Nothing opens or blocks traffic by it. Long downloads run as a Run
(ctx.runs), not inside an RPC method: a call times out after 30 s by
default, and a method with the default lock="scene" holds the Scene lock
while it runs.
Heavy data#
Do not send arrays through RPC as JSON lists. Write them to a Project file
(or a Result) and pass the Project path; the web half loads it through
resources or arrays. Live data goes through ctx.streams.