studio-engine 0.76.3 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +18 -0
- data/lib/studio/s3.rb +47 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +24 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c579a6d057a79eb540fee2347d6000c6c5af80273e61efdbd2e1d8eda173214
|
|
4
|
+
data.tar.gz: 726c8217e6f23e277af7f3fcc215885d7bfed504ee2a215b574aa5775986ffad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 72b688c3d92118f08ea7ba2263db3973fedeec0d16d07d9f45296c9c68fc581e6f5d56f30eb6d3375f0dedff9d1330de6ed992f5080995ddb44aa0ec6b79eae1
|
|
7
|
+
data.tar.gz: cf4ed66373933628dccd7eeb8e59164da68cfcc25f3407befff234ebd18ce2793a2d60bfb71daae219ec8980f5f7ada6b3b34f092697d4dbcb8894f451acd162
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.77.0 — 2026-09-26
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Studio::S3` runs on Cloudflare R2, or any S3-compatible endpoint.** Four new
|
|
12
|
+
settings, all nil by default: `s3_endpoint`, `s3_access_key_id` +
|
|
13
|
+
`s3_secret_access_key` (passed to the client as a pair, so an app can hold its
|
|
14
|
+
R2 keys beside the `AWS_*` ones during a cutover; half a pair raises), and
|
|
15
|
+
`s3_public_url`, the base public objects are served from. Unset, every app gets
|
|
16
|
+
the same client and the same amazonaws.com URLs as before. On an endpoint with
|
|
17
|
+
no public base, `url` raises `NotConfigured` instead of building a URL R2 would
|
|
18
|
+
refuse, and `upload` still writes but returns `nil`. Proven live against an R2
|
|
19
|
+
bucket: upload, download, exists, list, signed URL (200) and delete, with the
|
|
20
|
+
anonymous endpoint read refused.
|
|
21
|
+
|
|
7
22
|
## 0.76.3 — 2026-09-23
|
|
8
23
|
|
|
9
24
|
### Fixed
|
data/README.md
CHANGED
|
@@ -838,6 +838,24 @@ config.s3_key_prefix = "mcritchie-industries/"
|
|
|
838
838
|
logical keys and never see it. Unset (the default) leaves keys byte-identical to
|
|
839
839
|
what every already-shipped app wrote.
|
|
840
840
|
|
|
841
|
+
### S3-compatible storage (Cloudflare R2)
|
|
842
|
+
|
|
843
|
+
`Studio::S3` talks to any S3-compatible endpoint. The McRitchie fleet is moving
|
|
844
|
+
to R2 (one account, McRitchie Studio's); an app switches in its initializer:
|
|
845
|
+
|
|
846
|
+
```ruby
|
|
847
|
+
config.s3_endpoint = ENV["R2_ENDPOINT"] # https://<account>.r2.cloudflarestorage.com
|
|
848
|
+
config.s3_region = "auto"
|
|
849
|
+
config.s3_access_key_id = ENV["R2_ACCESS_KEY_ID"]
|
|
850
|
+
config.s3_secret_access_key = ENV["R2_SECRET_ACCESS_KEY"]
|
|
851
|
+
config.s3_public_url = ENV["R2_PUBLIC_URL"] # custom domain on this env's bucket
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
All four are nil by default, and nil is AWS exactly as before. The keys must be
|
|
855
|
+
set as a pair. R2 serves nothing anonymously from its S3 endpoint, so with an
|
|
856
|
+
endpoint and no `s3_public_url`, `Studio::S3.url` raises `NotConfigured` and
|
|
857
|
+
`upload` writes but returns `nil`; serve private objects with `signed_url`.
|
|
858
|
+
|
|
841
859
|
An app with **no** bucket configured does not error — `/admin/emails` renders
|
|
842
860
|
read-only, showing the inherited defaults it is genuinely sending and naming the
|
|
843
861
|
one setting that turns uploads on.
|
data/lib/studio/s3.rb
CHANGED
|
@@ -15,17 +15,34 @@ module Studio
|
|
|
15
15
|
opts[:content_type] = content_type if content_type
|
|
16
16
|
opts[:cache_control] = cache_control if cache_control
|
|
17
17
|
client.put_object(**opts)
|
|
18
|
-
url(key: key)
|
|
18
|
+
public_url? ? url(key: key) : nil
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def download(key:)
|
|
22
22
|
client.get_object(bucket: bucket, key: full_key(key)).body.read
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The PUBLIC URL of an object. On AWS (no endpoint) it is the bucket's
|
|
26
|
+
# virtual-hosted amazonaws.com URL, byte-identical to every version before
|
|
27
|
+
# endpoints existed. On an S3-compatible endpoint it is Studio.s3_public_url
|
|
28
|
+
# plus the key, and without that base it RAISES: R2's S3 endpoint answers no
|
|
29
|
+
# anonymous request, so any URL built from it would be a broken image in an
|
|
30
|
+
# inbox, not an error anyone sees. Private objects want signed_url.
|
|
25
31
|
def url(key:)
|
|
32
|
+
base = Studio.s3_public_url.to_s
|
|
33
|
+
return "#{base.chomp("/")}/#{full_key(key)}" unless base.empty?
|
|
34
|
+
raise NotConfigured, "Studio.s3_public_url not set: #{endpoint} serves no public URL (use signed_url for private objects)" if endpoint
|
|
35
|
+
|
|
26
36
|
"https://#{bucket}.s3.#{region}.amazonaws.com/#{full_key(key)}"
|
|
27
37
|
end
|
|
28
38
|
|
|
39
|
+
# Whether url can answer. upload asks this so an app with only private
|
|
40
|
+
# objects (a data room on R2 with no public domain) can still write:
|
|
41
|
+
# upload returns nil there instead of raising AFTER the object landed.
|
|
42
|
+
def public_url?
|
|
43
|
+
endpoint.nil? || !Studio.s3_public_url.to_s.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
29
46
|
def signed_url(key:, expires_in: 3600)
|
|
30
47
|
require "aws-sdk-s3"
|
|
31
48
|
Aws::S3::Presigner.new(client: client).presigned_url(:get_object, bucket: bucket, key: full_key(key), expires_in: expires_in)
|
|
@@ -93,11 +110,39 @@ module Studio
|
|
|
93
110
|
Studio.s3_region
|
|
94
111
|
end
|
|
95
112
|
|
|
113
|
+
# The S3-compatible endpoint (R2: https://<account>.r2.cloudflarestorage.com),
|
|
114
|
+
# or nil for AWS. Blank reads as unset, so an empty ENV var cannot point the
|
|
115
|
+
# client at "".
|
|
116
|
+
def endpoint
|
|
117
|
+
value = Studio.s3_endpoint.to_s
|
|
118
|
+
value.empty? ? nil : value
|
|
119
|
+
end
|
|
120
|
+
|
|
96
121
|
def client
|
|
97
122
|
@client ||= begin
|
|
98
123
|
require "aws-sdk-s3"
|
|
99
|
-
Aws::S3::Client.new(
|
|
124
|
+
Aws::S3::Client.new(**client_options)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Only what is configured is passed, so an unconfigured app builds exactly
|
|
129
|
+
# the client it always did: region alone, the SDK's default credential chain.
|
|
130
|
+
# Keys are passed as a PAIR or not at all; half a pair is a configuration
|
|
131
|
+
# error, and falling back to the default chain would silently write with
|
|
132
|
+
# whatever AWS_* key the dyno happens to hold.
|
|
133
|
+
def client_options
|
|
134
|
+
opts = { region: region }
|
|
135
|
+
opts[:endpoint] = endpoint if endpoint
|
|
136
|
+
id = Studio.s3_access_key_id.to_s
|
|
137
|
+
secret = Studio.s3_secret_access_key.to_s
|
|
138
|
+
if id.empty? != secret.empty?
|
|
139
|
+
raise NotConfigured, "Studio.s3_access_key_id and s3_secret_access_key must be set together"
|
|
140
|
+
end
|
|
141
|
+
unless id.empty?
|
|
142
|
+
opts[:access_key_id] = id
|
|
143
|
+
opts[:secret_access_key] = secret
|
|
100
144
|
end
|
|
145
|
+
opts
|
|
101
146
|
end
|
|
102
147
|
|
|
103
148
|
def reset!
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -616,6 +616,30 @@ module Studio
|
|
|
616
616
|
# A trailing slash is added if you leave it off.
|
|
617
617
|
mattr_accessor :s3_key_prefix, default: nil
|
|
618
618
|
|
|
619
|
+
# S3-COMPATIBLE STORAGE (Cloudflare R2). All four default nil, and nil means
|
|
620
|
+
# AWS exactly as before: the SDK's own endpoint, its default credential chain
|
|
621
|
+
# (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY), and the virtual-hosted
|
|
622
|
+
# amazonaws.com URL. An app moves to R2 by setting them, typically from ENV:
|
|
623
|
+
#
|
|
624
|
+
# config.s3_endpoint = ENV["R2_ENDPOINT"] # https://<account>.r2.cloudflarestorage.com
|
|
625
|
+
# config.s3_region = "auto" # R2's only region
|
|
626
|
+
# config.s3_access_key_id = ENV["R2_ACCESS_KEY_ID"]
|
|
627
|
+
# config.s3_secret_access_key = ENV["R2_SECRET_ACCESS_KEY"]
|
|
628
|
+
# config.s3_public_url = ENV["R2_PUBLIC_URL"] # https://assets.example.com
|
|
629
|
+
#
|
|
630
|
+
# Explicit keys exist so an app can hold its R2 pair BESIDE its AWS pair
|
|
631
|
+
# during a cutover (Active Storage may still mirror to S3) without the two
|
|
632
|
+
# fighting over the same AWS_* variables.
|
|
633
|
+
#
|
|
634
|
+
# s3_public_url is the base public objects are served from — a custom domain
|
|
635
|
+
# or r2.dev URL attached to THIS environment's bucket. R2's S3 endpoint
|
|
636
|
+
# serves nothing anonymously, so with an endpoint and no public base there is
|
|
637
|
+
# no public URL to give, and Studio::S3.url says so rather than inventing one.
|
|
638
|
+
mattr_accessor :s3_endpoint, default: nil
|
|
639
|
+
mattr_accessor :s3_access_key_id, default: nil
|
|
640
|
+
mattr_accessor :s3_secret_access_key, default: nil
|
|
641
|
+
mattr_accessor :s3_public_url, default: nil
|
|
642
|
+
|
|
619
643
|
# Knowledge layer — the S3-backed document store + /admin/knowledge browser
|
|
620
644
|
# (Studio::KnowledgeDoc). Routes are opt-in like every route surface.
|
|
621
645
|
# knowledge_agents is the roster of agent slugs the intake UI offers
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.77.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|