class Sepia::FileStorage

Overview

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

Defined in:

sepia/file_storage.cr

Constructors

Instance Method Summary

Instance methods inherited from class Sepia::StorageBackend

clear clear, count(object_class : Class) : Int32 count, delete(class_name : String, id : String)
delete(object : Serializable | Container)
delete
, exists?(object_class : Class, id : String) : Bool exists?, export_data : Hash(String, Array(Hash(String, String))) export_data, import_data(data : Hash(String, Array(Hash(String, String)))) import_data, list_all(object_class : Class) : Array(String) list_all, list_all_objects : Hash(String, Array(String)) list_all_objects, load(object_class : Class, id : String, path : String | Nil = nil) : Object load, save(object : Serializable, path : String | Nil = nil)
save(object : Container, path : String | Nil = nil)
save

Constructor Detail

def self.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,
})

[View source]

Instance Method Detail

def 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

[View source]
def 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"

[View source]
def 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")

[View source]
def 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

[View source]
def 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

[View source]
def export_data : Hash(String, Array(Hash(String, String))) #

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"}
#   ]
# }

[View source]
def 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)

[View source]
def 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"]

[View source]
def list_all_objects : Hash(String, Array(String)) #

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"]
# }

[View source]
def 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")

[View source]
def 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

[View source]
def on_watcher_change(&block : Event -> ) #

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

[View source]
def 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"

[View source]
def 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"

[View source]
def 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

[View source]
def 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/

[View source]
def start_watcher : Bool #

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?}"

[View source]
def stop_watcher : Bool #

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?}"

[View source]
def watcher : Watcher | Nil #

[View source]
def watcher_enabled? : Bool #

Configuration for watcher creation.

Stores the watcher configuration for later use or recreation.

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

[View source]
def watcher_running? : Bool #

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

[View source]