c3b-assets 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # c3b-assets
2
2
 
3
- Fingerprints a project's images and syncs them to a [Bunny](https://bunny.net) storage zone, so images never live in
4
- git and never need uploading by hand. Writes `d8e-asset-manifest.json`, which [d8e](https://codeberg.org/colab47/d8e)
3
+ Fingerprints a project's images, audio and video and syncs them to a [Bunny](https://bunny.net) storage zone, so
4
+ media never lives in git and never needs uploading by hand. Writes `d8e-asset-manifest.json`, which [d8e](https://codeberg.org/colab47/d8e)
5
5
  (1.2.0+) uses to point every reference at the CDN.
6
6
 
7
7
  ## How it works
@@ -23,7 +23,8 @@ images out of `dist/`.
23
23
 
24
24
  It only ever lists, uploads to and deletes inside the zone's `colab-asset-manager/` folder (the tool's old name,
25
25
  kept so existing zones need no re-upload) — files uploaded by hand anywhere else in the zone are never touched.
26
- Supported: `avif`, `gif`, `jpeg`, `jpg`, `png`, `svg`, `webp`.
26
+ Supported: images `avif`, `gif`, `jpeg`, `jpg`, `png`, `svg`, `webp`; audio `mp3`, `m4a`, `ogg`, `wav`; video `mp4`,
27
+ `webm`. Anything else under `src/asset/` (fonts, say) stays in git and in `dist/`.
27
28
 
28
29
  It runs on your machine only, never at deploy: the storage key stays off Heroku and Vercel, and staging can't delete
29
30
  production's images.
@@ -51,7 +52,7 @@ npm install -g c3b-assets@latest
51
52
  `uk.`, `ny.`, `la.`, `sg.`, `se.`, `br.`, `jh.` (Johannesburg) or `syd.storage.bunnycdn.com`.
52
53
  - `cdnUrl`: the pull zone connected to that storage zone.
53
54
 
54
- ### 3. Gitignore the images
55
+ ### 3. Gitignore the media
55
56
 
56
57
  ```
57
58
  src/asset/**/*.avif
@@ -61,6 +62,12 @@ src/asset/**/*.jpg
61
62
  src/asset/**/*.png
62
63
  src/asset/**/*.svg
63
64
  src/asset/**/*.webp
65
+ src/asset/**/*.mp3
66
+ src/asset/**/*.m4a
67
+ src/asset/**/*.ogg
68
+ src/asset/**/*.wav
69
+ src/asset/**/*.mp4
70
+ src/asset/**/*.webm
64
71
  ```
65
72
 
66
73
  If the images are already committed, also run `git rm -r --cached src/asset` once (it keeps your local copies). Their
@@ -119,7 +126,7 @@ into `src/asset/` (`c3b-assets s` on each) and run `c3b-assets u` again — it r
119
126
  those deletions is deleted with them (Bunny keeps empty folders otherwise).
120
127
  - Only files this project's manifest has listed are ever retired or deleted. If two projects share a storage zone,
121
128
  neither touches the other's images.
122
- - The download step of `s` only fetches image files, and only from this project's `cdnUrl` folder — the manifest is
129
+ - The download step of `s` only fetches image, audio and video files, and only from this project's `cdnUrl` folder — the manifest is
123
130
  a committed file, so a tampered entry can't plant something else in `src/asset/`.
124
131
  - `storageHost` must be a Bunny storage endpoint (`storage.bunnycdn.com`, optionally region-prefixed): the storage
125
132
  key is sent to it, and the config is committed.
package/adopt.js CHANGED
@@ -4,14 +4,14 @@
4
4
  // is asset/arch/x.webp at ${ cdnUrl }/colab-asset-manager/arch/x.2ed3ab61.webp. Only images; a
5
5
  // key stored under more than one fingerprint keeps the most recently changed copy and retires
6
6
  // the rest from today, as an upload would have.
7
- export function manifestFromZone(entries, { cdnUrl, folder, imageExtensions, today }) {
7
+ export function manifestFromZone(entries, { cdnUrl, folder, assetExtensions, today }) {
8
8
  const chosen = new Map();
9
9
  const retired = {};
10
10
  const skipped = [];
11
11
 
12
12
  for (const entry of entries) {
13
13
  const match = entry.path.match(/^(.*)\.([0-9a-f]{8})\.([^./]+)$/);
14
- if (!match || !imageExtensions.includes(match[3].toLowerCase()) || !entry.path.startsWith(`${ folder }/`)) {
14
+ if (!match || !assetExtensions.includes(match[3].toLowerCase()) || !entry.path.startsWith(`${ folder }/`)) {
15
15
  skipped.push(entry.path);
16
16
  continue;
17
17
  }
package/c3b-assets.js CHANGED
@@ -11,7 +11,7 @@ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), 'package.j
11
11
  const { version } = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
12
12
  console.log(`c3b-assets version: ${ version }`);
13
13
 
14
- // Fingerprints every image under src/asset/, uploads the ones Bunny doesn't have yet, and writes
14
+ // Fingerprints every image, audio and video file under src/asset/, uploads the ones Bunny doesn't have yet, and writes
15
15
  // d8e-asset-manifest.json (asset path under src/ -> CDN URL) for d8e to rewrite references with.
16
16
  // Files in Bunny that nothing references any more are deleted once they've been unreferenced
17
17
  // for GRACE_DAYS — pages already in browser caches, sent emails and older staging builds still
@@ -24,7 +24,9 @@ const SRC = resolve('src');
24
24
  const ASSET_DIR = resolve(SRC, 'asset');
25
25
  const CONFIG = resolve('c3b-assets.json');
26
26
  const MANIFEST = resolve('d8e-asset-manifest.json');
27
- const IMAGE_EXTENSIONS = ['avif', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp'];
27
+ // what c3b-assets manages: images, audio and video; anything else under src/asset/ (fonts, say)
28
+ // stays in git and in dist/
29
+ const ASSET_EXTENSIONS = ['avif', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp', 'mp3', 'm4a', 'ogg', 'wav', 'mp4', 'webm'];
28
30
 
29
31
  // Everything c3b-assets uploads lives under this folder in the storage zone, and it only ever
30
32
  // lists or deletes inside it — files uploaded by hand elsewhere in the zone are never touched.
@@ -37,8 +39,8 @@ const DAY_MS = 24 * 60 * 60 * 1000;
37
39
  // The commands and flags the tool accepts, each with its one-line description. `--help` prints them;
38
40
  // anything not listed here is rejected rather than silently ignored.
39
41
  const COMMANDS = {
40
- 'u': 'upload: fingerprint src/asset/, upload new images, write d8e-asset-manifest.json, delete retired files',
41
- 's': 'sync: download images the manifest lists that are missing locally, then upload (as "u")',
42
+ 'u': 'upload: fingerprint src/asset/, upload new files, write d8e-asset-manifest.json, delete retired files',
43
+ 's': 'sync: download files the manifest lists that are missing locally, then upload (as "u")',
42
44
  'a': 'adopt: for a checkout with no manifest, rebuild d8e-asset-manifest.json from what the zone holds, then download'
43
45
  };
44
46
  const FLAGS = {
@@ -120,7 +122,7 @@ function readConfig() {
120
122
  // Bunny. The fingerprint comes from the content, so changing an image always gives it a new name.
121
123
  function scanImages() {
122
124
  const images = [];
123
- for (const file of globSync(`${ ASSET_DIR }/**/*.{${ IMAGE_EXTENSIONS.join(',') }}`)) {
125
+ for (const file of globSync(`${ ASSET_DIR }/**/*.{${ ASSET_EXTENSIONS.join(',') }}`)) {
124
126
  const content = readFileSync(file);
125
127
  const hash = fingerprint(content);
126
128
  const ext = extname(file);
@@ -212,7 +214,7 @@ function requireStorageKey() {
212
214
  function status() {
213
215
  console.log(`zone ${ config.storageZone } -> ${ config.cdnUrl }`);
214
216
  if (!existsSync(ASSET_DIR)) {
215
- console.log(`${ ASSET_DIR } not found — run "c3b-assets s" to download the manifest's images`);
217
+ console.log(`${ ASSET_DIR } not found — run "c3b-assets s" to download the manifest's files`);
216
218
 
217
219
  return;
218
220
  }
@@ -223,7 +225,7 @@ function status() {
223
225
  const retired = Object.entries(previous.retired ?? {});
224
226
  const due = retired.filter(([, since]) => (Date.now() - new Date(since)) / DAY_MS >= GRACE_DAYS).length;
225
227
 
226
- console.log(`${ images.length } images in src/asset/, ${ Object.keys(previous.assets ?? {}).length } in the manifest`);
228
+ console.log(`${ images.length } files in src/asset/, ${ Object.keys(previous.assets ?? {}).length } in the manifest`);
227
229
  for (const key of added) {
228
230
  console.log(` new ${ key }`);
229
231
  }
@@ -241,10 +243,10 @@ function status() {
241
243
  if (toUpload === 0 && missing.length === 0 && due === 0) {
242
244
  console.log('Everything in src/asset/ is uploaded and in the manifest.');
243
245
  } else if (missing.length > 0) {
244
- console.log(`Run "c3b-assets s" to download ${ missing.length } missing image(s)`
246
+ console.log(`Run "c3b-assets s" to download ${ missing.length } missing file(s)`
245
247
  + (toUpload > 0 ? ` and upload ${ toUpload }.` : '.'));
246
248
  } else {
247
- console.log(`Run "c3b-assets u" to upload ${ toUpload } image(s)`
249
+ console.log(`Run "c3b-assets u" to upload ${ toUpload } file(s)`
248
250
  + (due > 0 ? `, with --allow-removals to delete ${ due } retired file(s).` : '.'));
249
251
  }
250
252
  }
@@ -263,7 +265,7 @@ async function upload(accessKey) {
263
265
  const localKeys = new Set(images.map((image) => image.key));
264
266
  const missing = Object.keys(previous.assets ?? {}).filter((key) => !localKeys.has(key));
265
267
  if (missing.length > 0 && !allowRemovals) {
266
- fail(`${ missing.length } image(s) in the manifest aren't in src/asset/ (e.g. ${ missing[0] }). `
268
+ fail(`${ missing.length } file(s) in the manifest aren't in src/asset/ (e.g. ${ missing[0] }). `
267
269
  + 'Run "c3b-assets s" to download them, or add --allow-removals if you removed them on purpose.');
268
270
  }
269
271
 
@@ -284,7 +286,7 @@ async function upload(accessKey) {
284
286
  + 'Add --allow-removals to delete them from Bunny, once this branch is what production serves.');
285
287
  }
286
288
 
287
- console.log(`${ images.length } images, ${ toUpload.length } to upload, ${ toDelete.length } to delete, `
289
+ console.log(`${ images.length } files, ${ toUpload.length } to upload, ${ toDelete.length } to delete, `
288
290
  + `${ Object.keys(retired).length } waiting out the ${ GRACE_DAYS }-day grace period`);
289
291
 
290
292
  for (const image of toUpload) {
@@ -389,9 +391,9 @@ async function pullImages() {
389
391
  fail(`refusing to write outside src/asset/: ${ key }`);
390
392
  }
391
393
  // The manifest is a committed file, so what it points at is only trusted as far as this
392
- // project's own CDN folder, and only ever written as an image.
393
- if (!IMAGE_EXTENSIONS.includes(extname(key).slice(1).toLowerCase())) {
394
- fail(`refusing to download ${ key }: not an image`);
394
+ // project's own CDN folder, and only ever written as an image, audio or video file.
395
+ if (!ASSET_EXTENSIONS.includes(extname(key).slice(1).toLowerCase())) {
396
+ fail(`refusing to download ${ key }: not an image, audio or video file`);
395
397
  }
396
398
  if (remotePathOf(url) === null) {
397
399
  fail(`refusing to download ${ key } from outside ${ CDN_PREFIX }: ${ url }`);
@@ -443,7 +445,7 @@ async function adopt() {
443
445
  const storage = createStorage({ host: config.storageHost, zone: config.storageZone, accessKey: requireStorageKey() });
444
446
  const entries = await storage.listEntries(FOLDER);
445
447
  const today = new Date().toISOString().slice(0, 10);
446
- const manifest = manifestFromZone(entries, { cdnUrl: config.cdnUrl, folder: FOLDER, imageExtensions: IMAGE_EXTENSIONS, today });
448
+ const manifest = manifestFromZone(entries, { cdnUrl: config.cdnUrl, folder: FOLDER, assetExtensions: ASSET_EXTENSIONS, today });
447
449
 
448
450
  for (const key of Object.keys(manifest.assets)) {
449
451
  console.log(` adopt ${ key }`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c3b-assets",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Fingerprints a project's images and syncs them to a Bunny storage zone, writing the d8e-asset-manifest.json d8e reads.",
5
5
  "license": "GPL-3.0",
6
6
  "type": "module",