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.
  • url is ephemeral. Store fileId in your domain records, not the URL. Resolve the URL fresh at download time if your use case requires it.

Use cases and rules

Use caseRecommended approach
Process an uploaded file in a commandopen(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 stepsSave each intermediate result; pass fileId between steps
Serve a download link to the userRetrieve SavedFile.url at request time; do not cache the URL
Access files cross-tenantNot permitted; accessor is scoped to the current tenant context
Stream large files without bufferingRead 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 foundopen(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 largesave(...) 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

PatternWhy it breaks
Autowiring an object storage client directlyBypasses tenant isolation, credential rotation, and audit; will not pass platform code review
Storing SavedFile.url in domain recordsURLs 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 tenantAccess denied at the accessor level; design cross-entity flows via command chaining with explicit handoff
Buffering the entire open() stream for large filesMemory pressure under concurrent command execution; stream incrementally where possible
Treating save() as idempotentEach 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; FileAccessor is obtained and used inside execute.
  • AiProviderAccessor — Same host-bridge pattern for LLM provider calls; often used alongside FileAccessor in 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 FileAccessor interface is identical across editions; the operational and compliance surfaces wrapped around it differ.

Next steps