Storage

Obsidian provides a unified storage facade built around named disks. Each disk is an isolated backend — local filesystem, S3, or any custom implementation. The active disk is resolved at runtime from environment variables; all operations are available directly on the StorageManager or on a specific disk via disk().

Environment configuration
# Root directory on the filesystem
STORAGE_LOCAL_ROOT=storage/app

# Base URL used to generate public file URLs
STORAGE_LOCAL_URL=https://example.com/storage

# Default disk (registered disks: local, public)
STORAGE_DISK=local
Pre-registered disks
Disk Path Use
local STORAGE_LOCAL_ROOT Private files
public STORAGE_LOCAL_ROOT/public Publicly accessible files
Basic operations
@Inject
private StorageManager storage;

storage.put("reports/q3.pdf", bytes);          // Write bytes
storage.put("exports/users.csv", inputStream); // Write a stream

byte[] content      = storage.get("reports/q3.pdf");    // Read as bytes
InputStream stream  = storage.stream("reports/q3.pdf"); // Open a stream

storage.exists("reports/q3.pdf"); // Check existence
storage.delete("reports/q3.pdf"); // Delete

List<String> files = storage.list("reports"); // List directory

String url = storage.url("reports/q3.pdf");
// → https://example.com/storage/reports/q3.pdf
Switching disks
// Default disk (configured via STORAGE_DISK)
storage.put("file.txt", bytes);

// Specific disk
storage.disk("public").put("images/banner.png", bytes);
storage.disk("local").delete("tmp/import.csv");
File uploads — MultipartFile
Part part = request.raw().getPart("avatar");
MultipartFile file = new MultipartFile(part);

storage.putFile("avatars", file);
// → avatars/550e8400-e29b-41d4-a716-446655440000.png

file.getOriginalName(); // "photo.png"
file.getContentType();  // "image/png"
file.getSize();         // 204800 (bytes)
file.getExtension();    // "png"
file.getInputStream();  // raw stream
Custom disk
public class S3Disk implements StorageDisk {

    @Override
    public void put(String path, byte[] content) { ... }

    @Override
    public void put(String path, InputStream stream) { ... }

    @Override
    public byte[] get(String path) { ... }

    // implement stream(), delete(), exists(), list(), url()
}

// Register at boot
manager.addDisk("s3", new S3Disk(...));
💡 Behaviour
  • putFile() always generates a UUID filename — the original name is not preserved on disk
  • LocalDisk creates parent directories automatically on every write
  • • Path traversal attempts (../) throw a StorageException immediately
  • StorageManager is a singleton and injectable via @Inject
  • • All failures throw the unchecked StorageException — no checked IOExceptions leak out