Files
PiCloud/crates/shared/src/services.rs
MechaCat02 6e132b6ee0 feat(v1.1.5): files SDK + files:* triggers
Filesystem-backed blob storage as the fifth concrete trigger kind.

- `files::collection(c).{create,head,get,update,delete,list}` Rhai SDK
  (blob in/out; metadata maps; missing-field throws naming the field).
- `FilesService` trait in picloud-shared; `FsFilesRepo` (atomic
  write: temp→fsync→rename→fsync-dir→DB; single-pass SHA-256;
  checksum-verified reads → Corrupted) + `FilesServiceImpl` in
  manager-core. Metadata in Postgres (0018), bytes on disk under
  PICLOUD_FILES_ROOT with 0o700 shard dirs.
- `files:*` trigger kind via the Layout-E pattern (0019: widen both
  CHECKs + files_trigger_details), TriggerEvent::Files (metadata only,
  no bytes), emit_files fan-out, dispatcher arm, admin endpoint
  POST /triggers/files (reuses validate_trigger_target).
- AppFilesRead/AppFilesWrite capabilities → script:read/script:write
  (seven-scope commitment held). AppPubsubPublish reserved for v1.1.6.
- Admin files API (list + delete) + dashboard Files view per app.

Cross-app isolation keyed on cx.app_id at every layer. ~45 new tests
(service in-memory, fs tempdir, bridge integration). No DB required
for the suite. publish_ephemeral and the orphan sweep stay deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 21:18:17 +02:00

121 lines
4.5 KiB
Rust

//! `Services` — bundle of stateful SDK service handles plumbed from the
//! host binary into every Rhai execution.
//!
//! Constructed once at startup in the picloud binary; cloned (cheap —
//! every field is an `Arc`) into the per-call sdk bridge so script
//! invocations don't need to re-resolve dependencies. The bundle is
//! handed to `executor-core::sdk::register_all` alongside an
//! `SdkCallCx` to wire each `::` namespace.
//!
//! v1.1.0 shipped this empty; v1.1.1 added the first two service fields
//! (`kv`, `dead_letters`) plus the `events` emitter that bound services
//! use to publish events into the triggers outbox. v1.1.3 adds the
//! `modules` field — the `ModuleSource` consulted by the per-call
//! `PicloudModuleResolver` to load `import`ed module scripts.
//!
//! `#[non_exhaustive]` so adding fields is a non-breaking change for
//! consumers that only *pattern-match* a `&Services`; only crates that
//! *construct* a `Services` (the picloud binary and tests) update.
use std::sync::Arc;
use crate::{
DeadLetterService, DocsService, FilesService, HttpService, KvService, ModuleSource,
NoopDeadLetterService, NoopDocsService, NoopEventEmitter, NoopFilesService, NoopHttpService,
NoopKvService, NoopModuleSource, ServiceEventEmitter,
};
/// SDK service bundle. See module docs for the lifecycle and the v1.1.x
/// expansion plan.
#[non_exhaustive]
pub struct Services {
/// KV store (v1.1.1). Backed by Postgres in the picloud binary;
/// in-memory in tests.
pub kv: Arc<dyn KvService>,
/// Document store (v1.1.2). Backed by Postgres in the picloud
/// binary; in-memory in tests.
pub docs: Arc<dyn DocsService>,
/// Dead-letter management (v1.1.1). Scripts get
/// `dead_letters::replay(id)` and `dead_letters::resolve(id, reason)`.
pub dead_letters: Arc<dyn DeadLetterService>,
/// Event emitter for the triggers outbox. Mutating service methods
/// (`KvService::set/delete`, `DocsService::create/update/delete`,
/// future `files::*`, etc.) call `events.emit(cx, event)` after
/// the write succeeds. The outbox-backed impl in
/// `manager-core::outbox_event_emitter` replaces v1.1.0's
/// `NoopEventEmitter`.
pub events: Arc<dyn ServiceEventEmitter>,
/// Module source (v1.1.3). The `PicloudModuleResolver` consults
/// this to load `kind = 'module'` scripts that other scripts
/// `import`. Backed by Postgres in the picloud binary; in-memory
/// fakes in resolver tests.
pub modules: Arc<dyn ModuleSource>,
/// Outbound HTTP (v1.1.4). Scripts get `http::{get,post,…}`.
/// Backed by a reqwest client with the SSRF deny-list resolver in
/// the picloud binary; `NoopHttpService` in tests that don't make
/// network calls.
pub http: Arc<dyn HttpService>,
/// Filesystem-backed blob storage (v1.1.5). Scripts get
/// `files::collection(name).{create,head,get,update,delete,list}`.
/// Backed by a Postgres-metadata + on-disk-bytes repo in the
/// picloud binary; `NoopFilesService` in tests that don't touch
/// files.
pub files: Arc<dyn FilesService>,
}
impl Services {
/// Construct a bundle from already-constructed `Arc<dyn …>` handles.
/// The picloud binary's `main` wires this up after the DB pool is
/// open; tests build it from in-memory fakes.
#[must_use]
pub fn new(
kv: Arc<dyn KvService>,
docs: Arc<dyn DocsService>,
dead_letters: Arc<dyn DeadLetterService>,
events: Arc<dyn ServiceEventEmitter>,
modules: Arc<dyn ModuleSource>,
http: Arc<dyn HttpService>,
files: Arc<dyn FilesService>,
) -> Self {
Self {
kv,
docs,
dead_letters,
events,
modules,
http,
files,
}
}
/// All-noop bundle for tests that build an `Engine` but don't
/// exercise the stateful services. Returns the same shape as
/// `Services::new` so callers can't accidentally rely on a stub
/// silently doing the right thing — every call into a noop
/// service surfaces an explicit error.
#[must_use]
pub fn with_noop_services() -> Self {
Self::new(
Arc::new(NoopKvService),
Arc::new(NoopDocsService),
Arc::new(NoopDeadLetterService),
Arc::new(NoopEventEmitter),
Arc::new(NoopModuleSource),
Arc::new(NoopHttpService),
Arc::new(NoopFilesService),
)
}
}
impl Default for Services {
fn default() -> Self {
Self::with_noop_services()
}
}