-
Notifications
You must be signed in to change notification settings - Fork 41
example desktop file storage
Recipes for the upload file type (real per-user desktop storage,
Experimental). Contract:
files-on-desktop.md → Real file storage.
The default gate is WordPress's own upload_files capability
(Authors and up). A trusted intranet can open storage to everyone:
add_filter( 'openstation_stored_files_upload_capability', static function () {
return 'read'; // every logged-in openstation user
} );add_filter( 'openstation_stored_files_user_quota_bytes', static function ( $quota, $user_id ) {
if ( user_can( $user_id, 'manage_options' ) ) {
return 0; // admins: unlimited
}
return 200 * MB_IN_BYTES;
}, 10, 2 );Over-quota uploads fail with openstation_stored_files_quota_exceeded.
Additions here genuinely widen the policy — the framework keeps
core's wp_check_filetype_and_ext() re-check in agreement via a
scoped upload_mimes hook (a plain mimes override could only
narrow):
add_filter( 'openstation_stored_files_allowed_mimes', static function ( $mimes ) {
$mimes['stl'] = 'model/stl';
$mimes['md'] = 'text/markdown';
return $mimes;
} );The executable denylist (php*, phtml, phar, dotfiles, …) still
applies on top and should stay that way.
add_action( 'openstation_stored_file_uploaded', static function ( $file_id, $placement_id, $user_id ) {
$file = openstation_stored_files_get( $file_id );
error_log( sprintf( 'user %d uploaded %s (%d bytes)', $user_id, $file['display_name'], $file['size_bytes'] ) );
}, 10, 3 );
// Download audit trail.
add_action( 'openstation_stored_file_downloaded', static function ( $file_id, $user_id ) {
do_action( 'my_audit_log', 'file-download', compact( 'file_id', 'user_id' ) );
}, 10, 2 );Single-file shares are read + download only, user principals only — the invite/accept flow mirrors folder sharing:
$share_id = openstation_stored_file_share_invite( $file_id, $owner_id, $recipient_user_id );
// Recipient's next heartbeat carries the invite; on accept the
// framework plants the tile at their desktop root.Listen to the same actions folder shares fire — the row carries
target_type => 'file':
add_action( 'openstation_files_share_accepted', static function ( $share_id, $row ) {
if ( 'file' === ( $row['target_type'] ?? 'folder' ) ) {
// A stored file share was accepted.
}
}, 10, 2 );The desktop sink fires the same os.drop.* chain the
Media Library sink does:
wp.os.hooks.addAction(
'os.drop.after-upload',
'my-plugin/uploads',
( { result } ) => {
if ( result && typeof result.storedFileId === 'number' ) {
console.log( 'desktop upload landed', result.storedFileId, result.placement );
}
},
);The folder-window preview pane renders images, video, and audio
uploads inline out of the box; other types show a no-preview note
plus a Download action. To preview a type the framework doesn't
handle, hook the (pre-existing) os.files.preview filter
and return your own element — it fully replaces the built-in for
that placement:
wp.os.hooks.addFilter(
'os.files.preview',
'my-plugin/pdf-preview',
( node, placement ) => {
if (
placement.file.type === 'upload' &&
placement.file.mime === 'application/pdf'
) {
const host = document.createElement( 'div' );
// Note: downloads are served with
// `Content-Disposition: attachment`, so an <iframe> will
// download rather than display. Fetch the bytes with
// wp.os.fetch and hand them to a renderer such as
// PDF.js instead.
myPlugin.mountPdfViewer( host, placement.file.ref );
return host;
}
return node; // Defer to the built-in for everything else.
},
);The serialized upload shape carries mime, kind
(image | video | audio | pdf | archive | text | file), and
sizeBytes to branch on.
.htaccess protects the storage dir on Apache only. On nginx add:
location ^~ /wp-content/uploads/os-files/ { deny all; }The extensionless UUID disk names and the authenticated PHP-served
downloads are the effective floor either way. Back up the database
and uploads/os-files/ together — the table maps names to
bytes.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config