class

Sepia::FileStorage

Inherits Sepia::StorageBackend < Reference < Object

Filesystem-based storage backend for Sepia objects.

This is the default storage backend that stores objects on the local filesystem. Serializable objects are stored as files, while Container objects are stored as directories with nested structures.

Directory Structure

The storage creates a directory structure like:

storage_path/
  ├── ClassName1/
  │   ├── object1_id     (Serializable object file)
  │   └── object2_id     (Serializable object file)
  └── ClassName2/
      ├── container1/    (Container directory)
      │   ├── data.json  (Primitive properties)
      │   └── refs/      (Reference files/symlinks)
      └── container2/    (Container directory)

Example

# Configure Sepia to use filesystem storage
Sepia::Storage.configure(:filesystem, {"path" => "./data"})

# Objects will be stored in ./data/ClassName/sepia_id

Constructors

new(path : String = Dir.tempdir, watch : Bool | Hash = false)

Creates a new FileStorage instance.

The path parameter specifies the root directory where objects will be stored. If not provided, uses the system's temporary directory.

The watch parameter enables automatic file system monitoring for external changes. When enabled, the storage will automatically invalidate cache entries when files are modified externally.

Parameters

  • path : Root directory for object storage (default: system temp directory)
  • watch : Watcher configuration (default: false - disabled)
    • false : Disable watcher
    • true : Enable watcher with default settings
    • Hash : Custom watcher configuration options

Examples

# Use system temp directory
storage = FileStorage.new

# Use custom directory
storage = FileStorage.new("./data")

# Enable watcher with default settings
storage = FileStorage.new("./data", watch: true)

# Enable watcher with custom settings
storage = FileStorage.new("./data", watch: {
  recursive: true,
  latency:   0.1,
})
Source

Instance methods

clear

Clears all objects from storage.

Removes the entire storage directory and recreates it. This permanently deletes all data - use with caution.

storage.clear # Deletes everything in the storage path
Source
count(object_class : Class) : Int32

Returns the count of objects for a given class.

This is equivalent to list_all(object_class).size but may be more efficient in some implementations.

count = storage.count(MyDocument)
puts "Found #{count} documents"
Source
delete(class_name : String, id : String)

Deletes an object by class name and ID.

Alternative method to delete objects without loading them first. Requires knowing whether the class is a Container or Serializable type.

# Delete without loading the object
storage.delete("MyDocument", "doc-uuid")
storage.delete("MyBoard", "board-uuid")
Source
delete(object : Serializable | Container)

Deletes an object from the filesystem.

Removes the object's file or directory. For Container objects, recursively removes the entire directory structure including all nested objects.

doc = MyDocument.load("doc-uuid")
storage.delete(doc) # Removes the file

board = Board.load("board-uuid")
storage.delete(board) # Removes the directory and all contents
Source
exists?(object_class : Class, id : String) : Bool

Checks if an object with the given ID exists.

For Serializable objects, checks if the file exists. For Container objects, checks if the directory exists.

if storage.exists?(MyDocument, "doc-uuid")
  puts "Document exists"
end
Source
export_data

Exports all data as a portable hash structure.

Returns a hash where keys are class names and values are arrays of object data. Each object includes its ID and either its content (for Serializable objects) or a container type marker.

Useful for backing up or migrating data between storage backends.

data = storage.export_data
# data = {
#   "MyDocument" => [
#     {"id" => "doc1", "content" => "Hello"},
#     {"id" => "doc2", "content" => "World"}
#   ],
#   "MyBoard" => [
#     {"id" => "board1", "type" => "container"}
#   ]
# }
Source
import_data(data : Hash(String, Array(Hash(String, String))))

Imports data from an exported hash structure.

Restores objects from the data structure created by export_data. Clears any existing data before importing.

Useful for restoring backups or migrating from another storage backend.

data = {
  "MyDocument" => [
    {"id" => "doc1", "content" => "Hello"},
  ],
}
storage.import_data(data)
Source
list_all(object_class : Class) : Array(String)

Lists all object IDs for a given class.

Returns an array of all object IDs found in the class directory. The IDs are sorted alphabetically.

ids = storage.list_all(MyDocument)
ids # => ["doc-uuid1", "doc-uuid2", "doc-uuid3"]
Source
list_all_objects

Lists all objects grouped by class name.

Returns a hash where keys are class names and values are arrays of object IDs for that class. This provides a complete inventory of all objects in storage.

Useful for administrative purposes, data migration, or debugging.

all_objects = storage.list_all_objects
# all_objects = {
#   "MyDocument" => ["doc1", "doc2"],
#   "MyBoard" => ["board1"],
#   "User" => ["user1", "user2", "user3"]
# }
Source
load(object_class : Class, id : String, path : String | Nil = nil) : Object

Loads an object from the filesystem.

Deserializes an object of the specified class with the given ID. For Serializable objects, reads the file content and uses the class's from_sepia method. For Container objects, reconstructs the object from its directory structure.

Raises an exception if the object is not found.

# Load a Serializable object
doc = storage.load(MyDocument, "doc-uuid")

# Load a Container object
board = storage.load(MyBoard, "board-uuid")
Source
load_with_cache(object_class : Class, id : String, path : String | Nil = nil) : Object

Loads an object with caching support.

Similar to the regular load method but integrates with the storage caching system for better performance when loading the same objects multiple times.

Parameters

  • object_class : The class of object to load
  • id : The object's unique identifier
  • path : Optional custom load path

Returns

The loaded object.

Example

# Load with caching (faster for repeated loads)
doc1 = storage.load_with_cache(MyDocument, "doc-123")
doc2 = storage.load_with_cache(MyDocument, "doc-123") # Returns cached instance
Source
on_watcher_change

Registers a callback to be called when external file changes are detected.

This allows users to add custom logic in addition to automatic cache invalidation.

Parameters

  • &block : Callback block that receives Event objects

Example

storage.on_watcher_change do |event|
  puts "External change: #{event.type} #{event.object_class}:#{event.object_id}"
end
Source
path

Root directory path where objects are stored.

Default is the system's temporary directory. Can be set to any absolute path.

storage = FileStorage.new
storage.path # => "/tmp"

# Custom path
storage = FileStorage.new("./my_data")
storage.path # => "./my_data"
Source
path=(path : String)

Root directory path where objects are stored.

Default is the system's temporary directory. Can be set to any absolute path.

storage = FileStorage.new
storage.path # => "/tmp"

# Custom path
storage = FileStorage.new("./my_data")
storage.path # => "./my_data"
Source
save(object : Serializable, path : String | Nil = nil)

Saves a Serializable object to the filesystem.

Writes the object's serialized content to a file. Creates any necessary parent directories. Uses atomic write operations to prevent corruption.

The object is saved to path/class_name/sepia_id if no specific path is provided.

Atomic Writes

The file is first written to a temporary file (with .tmp extension), then atomically renamed to the final path. This prevents partial writes and ensures data integrity.

doc = MyDocument.new("Hello")
storage = FileStorage.new("./data")
storage.save(doc) # Creates ./data/MyDocument/uuid
Source
save(object : Container, path : String | Nil = nil)

Saves a Container object to the filesystem.

Creates a directory for the container and saves all nested objects and references. The container's primitive properties are saved to a data.json file, while nested Sepia objects are saved as files or symlinks.

board = Board.new("My Board")
storage = FileStorage.new("./data")
storage.save(board) # Creates ./data/Board/uuid/
Source
start_watcher

Starts the file system watcher if it's not already running.

Returns

true if watcher was started, false if already running or not configured

Example

storage.start_watcher
puts "Watcher running: #{storage.watcher_running?}"
Source
stop_watcher

Stops the file system watcher if it's running.

Returns

true if watcher was stopped, false if not running

Example

storage.stop_watcher
puts "Watcher running: #{storage.watcher_running?}"
Source
watcher
Source
watcher_enabled?

Configuration for watcher creation.

Stores the watcher configuration for later use or recreation.

storage = FileStorage.new("./data", watch: true)
storage.watcher_enabled? # => true
Source
watcher_running?

Checks if the file system watcher is currently running.

Returns

true if watcher is running, false otherwise

Example

storage = FileStorage.new("./data", watch: true)
puts storage.watcher_running? # => true
Source

Nested types