FileAccessor
FileAccessor is the plugin-facing bridge for reading and writing platform-managed files. It lets a command handler open an uploaded file by its platform file ID and persist generated output as a new platform file — without touching object storage clients, tenant bucket resolution, or credential management. The host owns those concerns; the accessor exposes a stable, side-effect-audited surface.
This is the file SPI behind every plugin capability that processes uploads, generates reports, converts documents, or produces derived artifacts. If your plugin needs to touch raw file bytes, this is the contract — not direct object storage access.
Concept
The accessor is delivered to plugin code as a host-bridge interface, the same pattern used by AiProviderAccessor and BackgroundAccessor. Plugins do not autowire storage clients — instead they declare a FileAccessor dependency and receive the host-bound instance at command execution time, via the well-known CommandContext.settings key:
String SETTINGS_KEY = "__fileAccessor";The host implementation resolves all calls against the current tenant context: which storage backend applies, which bucket and prefix the tenant owns, which access credentials are in scope, and how generated files are tracked in the platform file registry.
This split is what makes plugin file I/O storage-safe by construction. The plugin describes intent; the platform decides identity, location, and protocol.
Interface signature
package com.auraboot.framework.plugin.extension;
import java.io.InputStream;
public interface FileAccessor {
/** Well-known key for FileAccessor in the command settings map. */
String SETTINGS_KEY = "__fileAccessor";
/**
* Open a platform file by its public file id.
*
* @param fileId platform file pid/id returned by the upload API
* @return readable file stream; caller must close it
*/
InputStream open(String fileId);
/**
* Persist generated bytes as a platform file.
*
* @param originalName suggested download filename
* @param contentType MIME type
* @param bytes file content
* @return saved platform file metadata
*/
SavedFile save(String originalName, String contentType, byte[] bytes);
/** Saved platform file metadata returned to plugin handlers. */
record SavedFile(String fileId, String originalName, long size, String url) {}
}SavedFile is immutable. fileId is the stable platform ID you can store in records or return to the caller. url is a pre-signed or proxy URL valid for the platform's configured TTL — do not store it long-term; resolve it fresh when serving downloads.
Resolving the accessor inside a command handler
The typical pattern is: a command form contains a file-upload field whose value is a platform file ID. The handler opens the file, processes it, saves the derived result, and writes the output file ID back to the context data.
@Extension
public class ConvertReportHandler implements CommandHandlerExtension {
@Override
public String getCommandType() {
return "docs:report.convert";
}
@Override
public Object execute(CommandContext ctx) throws Exception {
FileAccessor files =
(FileAccessor) ctx.settings().get(FileAccessor.SETTINGS_KEY);
// 1. Read the uploaded source file by its platform file ID
String sourceFileId = (String) ctx.payload().get("sourceFileId");
byte[] converted;
try (InputStream in = files.open(sourceFileId)) {
converted = convertToTargetFormat(in); // plugin business logic
}
// 2. Persist the derived output as a new platform file
FileAccessor.SavedFile output = files.save(
"converted-report.pdf",
"application/pdf",
converted
);
// 3. Write output metadata back for downstream pipeline phases
ctx.payload().put("outputFileId", output.fileId());
ctx.payload().put("outputUrl", output.url());
ctx.payload().put("outputSize", output.size());
return output.fileId();
}
}Three things to notice:
- No storage client autowiring. The plugin never sees bucket names, credentials, or storage SDK classes.
- Always close the
InputStream.open(fileId)returns a live stream backed by platform storage; leaking it will exhaust connections under load. Use try-with-resources. urlis ephemeral. StorefileIdin your domain records, not the URL. Resolve the URL fresh at download time if your use case requires it.
Use cases and rules
| Use case | Recommended approach |
|---|---|
| Process an uploaded file in a command | open(fileId) with try-with-resources; fileId comes from a form field |
| Produce a generated artifact (PDF, CSV, image) | save(name, contentType, bytes) then store the returned fileId |
| Chain multiple conversion steps | Save each intermediate result; pass fileId between steps |
| Serve a download link to the user | Retrieve SavedFile.url at request time; do not cache the URL |
| Access files cross-tenant | Not permitted; accessor is scoped to the current tenant context |
| Stream large files without buffering | Read from open() stream incrementally; do not buffer entire file in memory if avoidable |
Error semantics
Both methods throw unchecked exceptions on failure because storage calls cross network and tenant-credential boundaries.
- File not found —
open(fileId)throws if the file ID does not exist in the tenant's file registry or the backing store has no object at that key. Do not swallow this; surface it to the user so they can re-upload. - Access denied — The file exists but belongs to a different tenant context. The platform enforces this at the accessor level; the plugin will never see cross-tenant bytes.
- Storage backend unavailable — Object storage is unreachable. The exception propagates through the command pipeline and is surfaced as a transient error. Do not implement plugin-side retry; the platform's retry and circuit-breaker policies apply at the host level.
- Payload too large —
save(...)rejects byte arrays that exceed the tenant's configured file size cap. Validate or split before calling.
Catch only what you can recover from. Wrapping storage exceptions into a generic message hides tenant-quota and backend-health signals from the platform's monitoring surface.
Audit and correlation
Every open and save call is logged to the platform's file audit trail, keyed by fileId, tenantId, commandType, and the command-pipeline correlation. No extra metadata API is required — the host bridges audit correlation from the command execution context automatically.
Do not log raw file bytes or file URLs from your handler; the platform's audit store handles provenance. Duplicate logging of URLs creates stale references after URL expiry.
Common pitfalls
| Pattern | Why it breaks |
|---|---|
| Autowiring an object storage client directly | Bypasses tenant isolation, credential rotation, and audit; will not pass platform code review |
Storing SavedFile.url in domain records | URLs are time-limited; stored URLs silently expire, breaking downloads |
Not closing the InputStream from open() | Leaks storage connections; manifests as timeout failures under load |
Calling open() on a file ID from a different tenant | Access denied at the accessor level; design cross-entity flows via command chaining with explicit handoff |
Buffering the entire open() stream for large files | Memory pressure under concurrent command execution; stream incrementally where possible |
Treating save() as idempotent | Each call creates a new platform file entry with a new fileId; deduplicate before saving if the command can retry |
Relationship to other SPIs
CommandHandlerExtension— How to participate in the Command Pipeline;FileAccessoris obtained and used insideexecute.AiProviderAccessor— Same host-bridge pattern for LLM provider calls; often used alongsideFileAccessorin document AI pipelines.BackgroundAccessor— Same host-bridge pattern for background-task system fields.
Enterprise note — Signed file artifact provenance (cryptographic chain-of-custody for generated files), encrypted-at-rest tenant-key options (per-tenant KMS-backed storage encryption), and long-term archive tier routing (automatic tiering of generated artifacts to cold storage per retention policy) are commercial-only capabilities. The open-source
FileAccessorinterface is identical across editions; the operational and compliance surfaces wrapped around it differ.
Next steps
- Command pipeline — pipeline phases that bracket your
open()andsave()calls - CommandHandlerExtension — the host of
FileAccessorusage - AiProviderAccessor — companion SPI for document AI pipelines
- Plugin Manifest — declaring file-handling commands in your plugin