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#

  1. 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.
  2. 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, and a/../b is read as b. It raises ProjectPathError for anything else, with a code: 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/) or root (a read, write or delete of the Project root itself). Turn it into RpcError("invalid_params", ...) in an RPC method. Studio's own Tools and imports check more strictly (traversal, hidden), so do not rely on a/../b in paths you hand to them.
  3. The web half never builds a URL. It loads bytes through the resources service with a ResourceRef. No fetch(, new WebSocket, window.witwin, localhost, http:// or getActiveProjectHttpBase in a web half; npm run lint:architecture fails on them in system Plugins.
  4. The user's own computer is not the server. A file the user picks or drops comes in through localFiles and is copied into the Project; the server never reads the user's disk.
  5. Ask what the connection can do; never guess from the host name. Use platform.has(...); the lint also fails on navigator.userAgent-style sniffing and isDesktop in 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' });
ResourceRefPoints 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.
  • fetch adds the connection's authorization; use it for data.
  • objectUrl gives a blob: URL for an <img> or a loader; call release when the element goes away.
  • signedUrl gives 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#

CapabilityTrue when
sameMachinethe ProjectServer runs on the user's computer (Local)
nativeDialogsthe desktop app's native file dialogs are available
revealLocal"Show in Explorer/Finder" works
serverProcessthe ProjectServer is a process the user runs (Local, Direct, Router), not a hosted sandbox
pluginInstallPlugins 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.