sanity-plugin-r2-video 0.1.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.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/cli.js +172 -0
- package/dist/chunk-D5VS4ED7.js +6 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +1 -0
- package/dist/studio/index.d.ts +395 -0
- package/dist/studio/index.js +1836 -0
- package/dist/studio/transcode.worker.d.ts +48 -0
- package/dist/studio/transcode.worker.js +132 -0
- package/package.json +88 -0
- package/worker/index.ts +181 -0
- package/worker/tsconfig.json +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paulo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# sanity-plugin-r2-video
|
|
2
|
+
|
|
3
|
+
Video for Sanity Studio, stored in Cloudflare R2 as a ladder of plain MP4s.
|
|
4
|
+
|
|
5
|
+
Encoding happens **in the browser** with [mediabunny](https://mediabunny.dev) —
|
|
6
|
+
WebCodecs, in a Web Worker. Each source becomes one MP4 per rendition tier, and
|
|
7
|
+
the first frame is uploaded to Sanity as a native image, so posters get the
|
|
8
|
+
Sanity CDN, `srcset`, `auto=format` and LQIP for free. A rendition is picked
|
|
9
|
+
once at load from the element's size, and never swapped.
|
|
10
|
+
|
|
11
|
+
## Installing
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add sanity-plugin-r2-video
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`sanity`, `react`, `react-dom`, `@sanity/ui`, `@sanity/icons` and
|
|
18
|
+
`styled-components` are peer dependencies. `mediabunny` is the only real
|
|
19
|
+
dependency.
|
|
20
|
+
|
|
21
|
+
Then add the plugin, and a Worker for it to talk to:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// sanity.config.ts
|
|
25
|
+
import { r2Video } from "sanity-plugin-r2-video/studio";
|
|
26
|
+
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
plugins: [
|
|
29
|
+
r2Video({
|
|
30
|
+
endpointUrl: "https://….workers.dev", // deployed Worker
|
|
31
|
+
token: "…", // matches the Worker's UPLOAD_TOKEN
|
|
32
|
+
bucketUrl: "https://….r2.dev", // origin renditions are served from
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Using it
|
|
39
|
+
|
|
40
|
+
Everything happens in the **R2 Video** tool in the Studio.
|
|
41
|
+
|
|
42
|
+
**Upload** — drop files onto the library, or press Upload. Files stage in a
|
|
43
|
+
list first; nothing encodes until you confirm.
|
|
44
|
+
|
|
45
|
+
**Settings** — collapsed by default. Open it to change the folder, keep audio,
|
|
46
|
+
or adjust quality for this batch only. **Preview** encodes just the tallest tier,
|
|
47
|
+
so you can see the size and quality those settings produce before committing.
|
|
48
|
+
|
|
49
|
+
**Details** — click a card. Plays the video, lists every rendition with its
|
|
50
|
+
size, and lets you rename it, move it to another folder, or delete it.
|
|
51
|
+
|
|
52
|
+
**Sync** — beside Upload. Compares the bucket against the library and offers to
|
|
53
|
+
remove anything no document points at.
|
|
54
|
+
|
|
55
|
+
In a document, add a video through any `asset` field: pick one from the library,
|
|
56
|
+
or upload without leaving the page.
|
|
57
|
+
|
|
58
|
+
## Configuring
|
|
59
|
+
|
|
60
|
+
Only `endpointUrl`, `token` and `bucketUrl` are required. Everything else falls
|
|
61
|
+
back to [`studio/defaults.ts`](studio/defaults.ts), shown here with its defaults.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
r2Video({
|
|
65
|
+
endpointUrl: "https://….workers.dev",
|
|
66
|
+
token: "…",
|
|
67
|
+
bucketUrl: "https://….r2.dev",
|
|
68
|
+
apiVersion: "2024-01-01",
|
|
69
|
+
tool: { name: "r2-video", title: "R2 Video" },
|
|
70
|
+
folders: {
|
|
71
|
+
type: "media.folder", // document type folders are read from
|
|
72
|
+
poster: "_R2 Video Posters", // where generated posters are filed
|
|
73
|
+
},
|
|
74
|
+
encoding: {
|
|
75
|
+
heights: [270, 360, 480, 720, 1080],
|
|
76
|
+
videoCodec: "avc",
|
|
77
|
+
audioCodec: "aac",
|
|
78
|
+
quality: 0.75,
|
|
79
|
+
preferBitrate: false,
|
|
80
|
+
nativeTopTier: false,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Encoding
|
|
86
|
+
|
|
87
|
+
**`heights`** is the tier ladder. A source shorter than a tier skips it; every
|
|
88
|
+
tier it does reach is another full encode.
|
|
89
|
+
|
|
90
|
+
**`quality`** maps to a **quantizer** for h264, not a bitrate: constant quality,
|
|
91
|
+
variable file size. `0.75` is QP 22, where h264 stops being distinguishable from
|
|
92
|
+
the source. `1` is QP 16, near-lossless, and routinely produces files **larger
|
|
93
|
+
than the source**.
|
|
94
|
+
|
|
95
|
+
**`preferBitrate`** flips that trade: predictable size, variable quality. A
|
|
96
|
+
1080p tier lands near 6.1 Mbps at `0.75` whatever the footage.
|
|
97
|
+
|
|
98
|
+
**`nativeTopTier`** copies the top rendition instead of re-encoding it, when its
|
|
99
|
+
height and codec already match the source — instant and bit-identical. Its size
|
|
100
|
+
is then whatever the source was exported at.
|
|
101
|
+
|
|
102
|
+
## Setting up a Worker
|
|
103
|
+
|
|
104
|
+
The Worker holds the R2 binding. Scaffold one — the binding name, entry point
|
|
105
|
+
and compatibility date are fixed, and the CLI fills in the rest:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pnpx sanity-plugin-r2-video setup worker
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Pass `--account`, `--bucket`, `--name` and `--origins` to skip the prompts.
|
|
112
|
+
|
|
113
|
+
Then install its dependencies, create the bucket, set the shared secret, and
|
|
114
|
+
deploy:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm add sanity-plugin-r2-video
|
|
118
|
+
pnpm add -D wrangler @cloudflare/workers-types
|
|
119
|
+
wrangler r2 bucket create my-bucket
|
|
120
|
+
openssl rand -base64 32
|
|
121
|
+
wrangler secret put UPLOAD_TOKEN --config r2-video-worker/wrangler.jsonc
|
|
122
|
+
wrangler deploy --config r2-video-worker/wrangler.jsonc
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Give `r2Video()` the deployed URL as `endpointUrl`, the same secret as `token`,
|
|
126
|
+
and the bucket's public URL as `bucketUrl`.
|
|
127
|
+
|
|
128
|
+
The generated entry point re-exports this package's handler, so upgrading the
|
|
129
|
+
package upgrades the deployed Worker. Only deployment identity — name, account,
|
|
130
|
+
bucket, origins — is written out.
|
|
131
|
+
|
|
132
|
+
## How it works
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Studio ──encoded renditions──▶ Worker ──binding──▶ R2 bucket
|
|
136
|
+
│ │
|
|
137
|
+
└──poster──▶ Sanity image assets bucket URL ─┘
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The Worker holds the only R2 binding, so **no R2 credentials exist outside
|
|
141
|
+
Cloudflare**. Uploads go through it as plain request bodies — nothing is signed,
|
|
142
|
+
and the bucket needs no CORS policy. A rendition has to fit in one request body,
|
|
143
|
+
which caps an upload at 100 MB.
|
|
144
|
+
|
|
145
|
+
`UPLOAD_TOKEN` ships inside the Studio bundle, since the browser is what sends
|
|
146
|
+
the upload — **anyone who can load the Studio can read it**. The Worker's origin
|
|
147
|
+
allowlist is what restricts access. Put Cloudflare Access in front of the Worker
|
|
148
|
+
if you need authentication.
|
|
149
|
+
|
|
150
|
+
### Entry points
|
|
151
|
+
|
|
152
|
+
Three, each with its own tsconfig and its own type universe:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
./studio the Sanity plugin react, react-dom, @sanity/ui
|
|
156
|
+
./worker the endpoint @cloudflare/workers-types
|
|
157
|
+
./storage where files live nothing
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`./storage` has no dependencies. A web app imports it to build source URLs
|
|
161
|
+
without pulling the Studio UI, or `sanity` and `react`, into its bundle.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { resolveRenditionPath } from "sanity-plugin-r2-video/storage";
|
|
165
|
+
|
|
166
|
+
const src = `${bucketUrl}/${resolveRenditionPath(asset._id, 720)}`;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Keys
|
|
170
|
+
|
|
171
|
+
One directory per video, one object per tier, no folder segment:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
j6w3wy2bd0jq/270.mp4
|
|
175
|
+
j6w3wy2bd0jq/360.mp4
|
|
176
|
+
j6w3wy2bd0jq/480.mp4
|
|
177
|
+
j6w3wy2bd0jq/720.mp4
|
|
178
|
+
j6w3wy2bd0jq/1080.mp4
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Documents store the id and the heights, not URLs — both the Studio and the web
|
|
182
|
+
build sources from `bucketUrl` plus the key. Moving the bucket behind a custom
|
|
183
|
+
domain is a config change, not a migration.
|
|
184
|
+
|
|
185
|
+
### Folders
|
|
186
|
+
|
|
187
|
+
Videos are filed in the **same folder documents the image library uses**, not a
|
|
188
|
+
parallel set. Create a folder in the Media tool and it appears in the video
|
|
189
|
+
picker; rename it there and every video moves with it.
|
|
190
|
+
|
|
191
|
+
The type is `folders.type`, defaulting to `sanity-plugin-media`'s
|
|
192
|
+
`media.folder`. The plugin never imports that package — it reads a document type
|
|
193
|
+
by name, so `folders.type` can point anywhere.
|
|
194
|
+
|
|
195
|
+
Keys carry no folder, so renaming or moving a video never touches the bucket.
|
|
196
|
+
The tool's filter lists only folders holding video; the upload dialog offers all
|
|
197
|
+
of them.
|
|
198
|
+
|
|
199
|
+
Generated posters go to their own folder (`folders.poster`), created on first
|
|
200
|
+
upload, so they stay out of the folders holding real images.
|
|
201
|
+
|
|
202
|
+
### Deleting
|
|
203
|
+
|
|
204
|
+
Order matters, and `delete-video.ts` documents it:
|
|
205
|
+
|
|
206
|
+
1. Preflight `*[references($id)]` — anything found blocks the delete.
|
|
207
|
+
2. Delete the document. This releases the strong reference to the poster.
|
|
208
|
+
3. Delete the poster asset. Now unreferenced, so no `409`.
|
|
209
|
+
4. Delete the R2 objects, batched.
|
|
210
|
+
|
|
211
|
+
Sanity goes before R2: an orphaned object is invisible and costs pennies, a
|
|
212
|
+
document pointing at deleted media breaks the site.
|
|
213
|
+
|
|
214
|
+
### When an upload fails
|
|
215
|
+
|
|
216
|
+
The document is written **last**, so a failure can never leave a video in the
|
|
217
|
+
library pointing at files that aren't there.
|
|
218
|
+
|
|
219
|
+
Anything created before that point is rolled back. Keys are recorded *before*
|
|
220
|
+
each upload, since a request that times out may still have stored the object.
|
|
221
|
+
Rollback never throws — the failure that started it is what surfaces.
|
|
222
|
+
|
|
223
|
+
**A closed tab or a crash skips rollback entirely**, and encoded renditions live
|
|
224
|
+
only in memory, so there's no resume. Whatever either case leaves behind is
|
|
225
|
+
unreferenced — **Sync** in the tool finds and removes it.
|
|
226
|
+
|
|
227
|
+
## Limitations
|
|
228
|
+
|
|
229
|
+
### Chrome only, for uploading
|
|
230
|
+
|
|
231
|
+
Uploading needs WebCodecs h264 encoding, which Safari doesn't reliably provide.
|
|
232
|
+
The gate is a capability check — mediabunny's own `canEncodeVideo('avc')`, not
|
|
233
|
+
user-agent sniffing — and unsupported browsers get told so before they pick a
|
|
234
|
+
file. **Playback is unaffected everywhere.**
|
|
235
|
+
|
|
236
|
+
### The encoder runs in a Web Worker
|
|
237
|
+
|
|
238
|
+
`transcode.worker.js` is loaded with `new URL(…, import.meta.url)`. Vite serves
|
|
239
|
+
that straight from the package, but if it ever pre-bundles the plugin the URL
|
|
240
|
+
goes stale — if uploads fail to start, exclude the package from dependency
|
|
241
|
+
optimisation:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// sanity.cli.ts
|
|
245
|
+
vite: (config) => ({
|
|
246
|
+
...config,
|
|
247
|
+
optimizeDeps: { exclude: ["sanity-plugin-r2-video"] },
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Developing
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pnpm install
|
|
255
|
+
pnpm run type-check
|
|
256
|
+
pnpm run build # tsup: ESM + types into dist
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`./worker` ships as TypeScript: wrangler compiles it, and the generated Worker
|
|
260
|
+
extends `worker/tsconfig.json` for the compiler options it was written against.
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
|
|
8
|
+
const USAGE = `
|
|
9
|
+
sanity-plugin-r2-video setup worker [directory]
|
|
10
|
+
|
|
11
|
+
Writes a deployable Cloudflare Worker: A wrangler config filled in from your
|
|
12
|
+
answers, and an entry point that re-exports this package's endpoint — so
|
|
13
|
+
upgrading the package upgrades the Worker.
|
|
14
|
+
|
|
15
|
+
directory Where to write it. Defaults to ./r2-video-worker
|
|
16
|
+
|
|
17
|
+
Options (prompted for when omitted, required when not a terminal):
|
|
18
|
+
|
|
19
|
+
--name Worker name default r2-video
|
|
20
|
+
--account Cloudflare account id required
|
|
21
|
+
--bucket R2 bucket name required
|
|
22
|
+
--origins Allowed Studio origins default http://localhost:3333
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
/** Flags parsed as `--key value`, so the command works unattended in CI. */
|
|
26
|
+
const parseFlags = (argv) => {
|
|
27
|
+
const flags = {};
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
30
|
+
const arg = argv[index];
|
|
31
|
+
|
|
32
|
+
if (arg.startsWith("--")) {
|
|
33
|
+
flags[arg.slice(2)] = argv[index + 1];
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return flags;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The Worker re-exports the package's handler rather than copying it, so
|
|
43
|
+
* upgrading the package upgrades the deployed endpoint. Only identity —
|
|
44
|
+
* name, account, bucket, origins — is generated.
|
|
45
|
+
*/
|
|
46
|
+
const ENTRY = `// The endpoint itself lives in the plugin, so upgrading the package upgrades
|
|
47
|
+
// this Worker. Only deployment identity belongs here, in \`wrangler.jsonc\`.
|
|
48
|
+
export { default } from "sanity-plugin-r2-video/worker";
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const TSCONFIG = `{
|
|
52
|
+
"$schema": "https://json.schemastore.org/tsconfig.json",
|
|
53
|
+
"extends": "sanity-plugin-r2-video/worker/tsconfig.json"
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const ask = async (rl, question, fallback) => {
|
|
58
|
+
if (!rl) {
|
|
59
|
+
return fallback ?? "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const suffix = fallback ? ` (${fallback})` : "";
|
|
63
|
+
const answer = (await rl.question(` ${question}${suffix}: `)).trim();
|
|
64
|
+
|
|
65
|
+
return answer || fallback || "";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Only the values that differ per deployment. The binding name, the entry point
|
|
70
|
+
* and the compatibility date are fixed — the Worker source expects them, so
|
|
71
|
+
* asking would only create a way to get them wrong.
|
|
72
|
+
*/
|
|
73
|
+
const createConfig = ({ name, accountId, bucket, origins }) => {
|
|
74
|
+
return `{
|
|
75
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
76
|
+
"name": ${JSON.stringify(name)},
|
|
77
|
+
"account_id": ${JSON.stringify(accountId)},
|
|
78
|
+
"main": "src/index.ts",
|
|
79
|
+
"compatibility_date": "2026-08-01",
|
|
80
|
+
|
|
81
|
+
// The binding is what keeps R2 credentials out of your repo entirely — the
|
|
82
|
+
// Worker reaches the bucket directly, so nothing has to be signed or stored
|
|
83
|
+
"r2_buckets": [
|
|
84
|
+
{
|
|
85
|
+
"binding": "BUCKET",
|
|
86
|
+
"bucket_name": ${JSON.stringify(bucket)}
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
"vars": {
|
|
91
|
+
// Studio origins allowed to call this Worker, comma separated
|
|
92
|
+
"ALLOWED_ORIGINS": ${JSON.stringify(origins)}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// UPLOAD_TOKEN is set separately, with: wrangler secret put UPLOAD_TOKEN
|
|
96
|
+
// Not because it's secret — the Studio ships the same value to browsers —
|
|
97
|
+
// but so it stays out of this file, out of git, and out of deploy logs
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const setupWorker = async (target, flags) => {
|
|
103
|
+
const directory = resolve(process.cwd(), target ?? "r2-video-worker");
|
|
104
|
+
|
|
105
|
+
if (existsSync(directory)) {
|
|
106
|
+
console.error(
|
|
107
|
+
`\n ${directory} already exists. Move it or pick another.\n`,
|
|
108
|
+
);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Prompt only for what wasn't passed, and only when there's someone to ask
|
|
114
|
+
const rl =
|
|
115
|
+
(!flags.account || !flags.bucket) && process.stdin.isTTY
|
|
116
|
+
? createInterface({ input: process.stdin, output: process.stdout })
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
const answers = {
|
|
120
|
+
name: flags.name ?? (await ask(rl, "Worker name", "r2-video")),
|
|
121
|
+
accountId: flags.account ?? (await ask(rl, "Cloudflare account id")),
|
|
122
|
+
bucket: flags.bucket ?? (await ask(rl, "R2 bucket name")),
|
|
123
|
+
origins:
|
|
124
|
+
flags.origins ??
|
|
125
|
+
(await ask(rl, "Allowed Studio origins", "http://localhost:3333")),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (rl) {
|
|
129
|
+
rl.close();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!answers.accountId || !answers.bucket) {
|
|
133
|
+
console.error(
|
|
134
|
+
"\n An account id and a bucket name are both required." +
|
|
135
|
+
"\n Pass --account and --bucket, or run this in a terminal.\n",
|
|
136
|
+
);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await mkdir(join(directory, "src"), { recursive: true });
|
|
142
|
+
await writeFile(join(directory, "src", "index.ts"), ENTRY);
|
|
143
|
+
await writeFile(join(directory, "tsconfig.json"), TSCONFIG);
|
|
144
|
+
await writeFile(join(directory, "wrangler.jsonc"), createConfig(answers));
|
|
145
|
+
|
|
146
|
+
const config = `${target ?? "r2-video-worker"}/wrangler.jsonc`;
|
|
147
|
+
|
|
148
|
+
console.info(`
|
|
149
|
+
Written to ${directory}
|
|
150
|
+
|
|
151
|
+
Next:
|
|
152
|
+
|
|
153
|
+
1. Install its dependencies there:
|
|
154
|
+
pnpm add sanity-plugin-r2-video
|
|
155
|
+
pnpm add -D wrangler @cloudflare/workers-types
|
|
156
|
+
2. wrangler secret put UPLOAD_TOKEN --config ${config}
|
|
157
|
+
3. wrangler deploy --config ${config}
|
|
158
|
+
4. Pass the deployed URL, the same token, and the bucket's public origin
|
|
159
|
+
to r2Video() in sanity.config.ts.
|
|
160
|
+
`);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const argv = process.argv.slice(2);
|
|
164
|
+
const [command, subcommand] = argv;
|
|
165
|
+
const target = argv[2] && !argv[2].startsWith("--") ? argv[2] : undefined;
|
|
166
|
+
|
|
167
|
+
if (command === "setup" && subcommand === "worker") {
|
|
168
|
+
await setupWorker(target, parseFlags(argv));
|
|
169
|
+
} else {
|
|
170
|
+
console.info(USAGE);
|
|
171
|
+
process.exitCode = command ? 1 : 0;
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the R2 object key for one rendition:
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* k3m9p2xq7wvt/1080.mp4
|
|
6
|
+
* └─── id ───┘ └height┘
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* One folder per video, one object per tier inside it. Deliberately flat - no
|
|
10
|
+
* folder segment, because folders are a Sanity concern only, so moving or
|
|
11
|
+
* renaming one never has to match anything stored.
|
|
12
|
+
*/
|
|
13
|
+
declare const resolveRenditionPath: (id: string, height: number) => string;
|
|
14
|
+
|
|
15
|
+
export { resolveRenditionPath };
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveRenditionPath } from './chunk-D5VS4ED7.js';
|