@zerotal/media 1.4.0 → 1.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/CHANGELOG.md +81 -1
- package/README.md +10 -6
- package/package.json +8 -6
- package/src/Media.ts +2 -3
- package/src/MediaItem.ts +1 -0
- package/src/collections/resolve.ts +5 -0
- package/src/collections/retention.ts +2 -0
- package/src/config.ts +1 -0
- package/src/conversions/BunImageDriver.ts +151 -43
- package/src/conversions/ConversionRunner.ts +5 -0
- package/src/conversions/ImageDriver.ts +46 -10
- package/src/conversions/SharpImageDriver.ts +58 -12
- package/src/conversions/dispatch.ts +3 -0
- package/src/conversions/raster.ts +510 -0
- package/src/errors.ts +27 -3
- package/src/index.ts +9 -21
- package/src/mediaSchemaConcern.ts +2 -0
- package/src/paths/PathGenerator.ts +1 -1
- package/src/sources.ts +8 -0
- package/src/support/disks.ts +4 -0
- package/src/testing.ts +35 -0
- package/src/types.ts +16 -2
package/src/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
// @zerotal/media — public API
|
|
2
|
+
//
|
|
3
|
+
// Test and advanced-wiring seams (state swapping, disk resolver overrides,
|
|
4
|
+
// queue-bridge internals) live at `@zerotal/media/testing`. They are
|
|
5
|
+
// deliberately not re-exported here: this entry point is what the package's
|
|
6
|
+
// stability guarantee covers, and it should stay small enough to keep.
|
|
2
7
|
|
|
3
8
|
// Model + model mixin
|
|
4
9
|
// `Media` is the mixin — it reads as `Model.using(Media)`. `MediaItem` is one
|
|
@@ -19,7 +24,7 @@ export type { CollectionHost } from "./collections/resolve.ts";
|
|
|
19
24
|
export { applyRetentionRules } from "./collections/retention.ts";
|
|
20
25
|
|
|
21
26
|
// Conversions
|
|
22
|
-
export { ConversionRunner
|
|
27
|
+
export { ConversionRunner } from "./conversions/ConversionRunner.ts";
|
|
23
28
|
export { BunImageDriver } from "./conversions/BunImageDriver.ts";
|
|
24
29
|
export { SharpImageDriver } from "./conversions/SharpImageDriver.ts";
|
|
25
30
|
export {
|
|
@@ -34,13 +39,7 @@ export type {
|
|
|
34
39
|
ImageMetadata,
|
|
35
40
|
ImageResult,
|
|
36
41
|
} from "./conversions/ImageDriver.ts";
|
|
37
|
-
export {
|
|
38
|
-
setConversionDispatcher,
|
|
39
|
-
isQueueAvailable,
|
|
40
|
-
dispatchConversions,
|
|
41
|
-
} from "./conversions/dispatch.ts";
|
|
42
|
-
export type { ConversionDispatcher } from "./conversions/dispatch.ts";
|
|
43
|
-
export { performConversions, ownerClassFor } from "./conversions/queueBridge.ts";
|
|
42
|
+
export { isQueueAvailable, dispatchConversions } from "./conversions/dispatch.ts";
|
|
44
43
|
|
|
45
44
|
// Paths
|
|
46
45
|
export { DefaultPathGenerator } from "./paths/PathGenerator.ts";
|
|
@@ -50,15 +49,8 @@ export type { PathGenerator } from "./paths/PathGenerator.ts";
|
|
|
50
49
|
export { fromValue, fromUrl, fromDisk, fromPath } from "./sources.ts";
|
|
51
50
|
export type { MediaSource, ResolvedSource, SourceResolver } from "./sources.ts";
|
|
52
51
|
|
|
53
|
-
// Disk resolution
|
|
54
|
-
export {
|
|
55
|
-
diskFor,
|
|
56
|
-
diskNameFor,
|
|
57
|
-
defaultDiskName,
|
|
58
|
-
setDiskResolver,
|
|
59
|
-
setDefaultDiskName,
|
|
60
|
-
} from "./support/disks.ts";
|
|
61
|
-
export type { DiskResolver } from "./support/disks.ts";
|
|
52
|
+
// Disk resolution
|
|
53
|
+
export { diskFor, defaultDiskName } from "./support/disks.ts";
|
|
62
54
|
|
|
63
55
|
// Schema provisioning
|
|
64
56
|
export { mediaSchemaConcern } from "./mediaSchemaConcern.ts";
|
|
@@ -67,10 +59,6 @@ export { mediaSchemaConcern } from "./mediaSchemaConcern.ts";
|
|
|
67
59
|
export { MediaConfig, mediaDefaults } from "./config.ts";
|
|
68
60
|
export type { MediaConfigShape } from "./config.ts";
|
|
69
61
|
|
|
70
|
-
// Shared state (mainly for tests and advanced wiring)
|
|
71
|
-
export { mediaState, setMediaState, resetMediaState } from "./state.ts";
|
|
72
|
-
export type { MediaState } from "./state.ts";
|
|
73
|
-
|
|
74
62
|
// Testing
|
|
75
63
|
export { MediaFake } from "./MediaFake.ts";
|
|
76
64
|
|
|
@@ -12,6 +12,8 @@ import { Schema } from "@zerotal/orm";
|
|
|
12
12
|
*
|
|
13
13
|
* Mirrors `auditSchemaConcern`, and exists for the same reason: the alternative
|
|
14
14
|
* is an app that boots cleanly and then fails on its first upload, in production.
|
|
15
|
+
*
|
|
16
|
+
* @internal — registered by MediaProvider so the `media` table provisions itself.
|
|
15
17
|
*/
|
|
16
18
|
export const mediaSchemaConcern: ConcernDescriptor = {
|
|
17
19
|
name: "media-schema",
|
|
@@ -25,7 +25,7 @@ export interface PathGenerator {
|
|
|
25
25
|
* media/<uuid>/responsive/640.webp
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
|
-
* Keyed on `uuid` rather than the numeric `id`
|
|
28
|
+
* Keyed on `uuid` rather than the numeric `id` a media library normally
|
|
29
29
|
* uses. These paths end up in public URLs, and a sequential id in a public URL
|
|
30
30
|
* discloses how many rows the table has — plus it lets anyone walk the range.
|
|
31
31
|
* The uuid costs nothing and leaks nothing.
|
package/src/sources.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { MediaError } from "./errors.ts";
|
|
|
3
3
|
import { diskFor } from "./support/disks.ts";
|
|
4
4
|
|
|
5
5
|
/** A file's bytes plus the name it arrived under. */
|
|
6
|
+
/** @internal — source-resolution plumbing. */
|
|
6
7
|
export interface ResolvedSource {
|
|
7
8
|
bytes: Uint8Array;
|
|
8
9
|
/** Original filename, used to derive the default media name. */
|
|
@@ -10,6 +11,7 @@ export interface ResolvedSource {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/** Anything that can be resolved to bytes on demand. */
|
|
14
|
+
/** @internal — source-resolution plumbing. */
|
|
13
15
|
export type SourceResolver = () => Promise<ResolvedSource>;
|
|
14
16
|
|
|
15
17
|
/** Things `addMedia()` accepts directly. */
|
|
@@ -21,6 +23,8 @@ export type MediaSource = UploadedFile | File | Blob | Uint8Array | ArrayBuffer;
|
|
|
21
23
|
* Resolution is deferred so a rule that can reject on metadata alone — a
|
|
22
24
|
* collection that accepts only PDFs, say — does not have to buffer the file
|
|
23
25
|
* first.
|
|
26
|
+
*
|
|
27
|
+
* @internal — source resolution; apps pass a `MediaSource` to `addMedia*`.
|
|
24
28
|
*/
|
|
25
29
|
export function fromValue(source: MediaSource, fileName?: string): SourceResolver {
|
|
26
30
|
if (source instanceof UploadedFile) {
|
|
@@ -66,6 +70,8 @@ export function fromValue(source: MediaSource, fileName?: string): SourceResolve
|
|
|
66
70
|
* often enough that downloading whatever arrives is how one request exhausts the
|
|
67
71
|
* heap; the limit is checked against `Content-Length` first and again against
|
|
68
72
|
* what actually arrived, since the header is a claim.
|
|
73
|
+
*
|
|
74
|
+
* @internal — source resolution; apps pass a `MediaSource` to `addMedia*`.
|
|
69
75
|
*/
|
|
70
76
|
export function fromUrl(url: string, maxBytes: number): SourceResolver {
|
|
71
77
|
return async () => {
|
|
@@ -108,6 +114,7 @@ export function fromUrl(url: string, maxBytes: number): SourceResolver {
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
/** Read a file already sitting on one of the app's storage disks. */
|
|
117
|
+
/** @internal — source resolution; apps pass a `MediaSource` to `addMedia*`. */
|
|
111
118
|
export function fromDisk(path: string, disk?: string): SourceResolver {
|
|
112
119
|
return async () => {
|
|
113
120
|
const bytes = await diskFor(disk).getBuffer(path);
|
|
@@ -119,6 +126,7 @@ export function fromDisk(path: string, disk?: string): SourceResolver {
|
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
/** Read a file from the local filesystem. */
|
|
129
|
+
/** @internal — source resolution; apps pass a `MediaSource` to `addMedia*`. */
|
|
122
130
|
export function fromPath(path: string): SourceResolver {
|
|
123
131
|
return async () => {
|
|
124
132
|
const file = Bun.file(path);
|
package/src/support/disks.ts
CHANGED
|
@@ -33,6 +33,8 @@ export function setDiskResolver(resolver: DiskResolver | null): void {
|
|
|
33
33
|
* empty string is looked up literally and throws `DiskNotConfiguredError`. Media
|
|
34
34
|
* config uses `""` to mean "inherit the default", so every lookup goes through
|
|
35
35
|
* here rather than reaching for `Storage.disk()` directly.
|
|
36
|
+
*
|
|
37
|
+
* @internal — disk resolution.
|
|
36
38
|
*/
|
|
37
39
|
export function diskFor(name?: string | null): StorageDriver {
|
|
38
40
|
const trimmed = name?.trim();
|
|
@@ -65,6 +67,8 @@ export function setDefaultDiskName(name: string | null): void {
|
|
|
65
67
|
*
|
|
66
68
|
* Falls back to `""` when there is no app or no storage config to ask — the read
|
|
67
69
|
* path treats that as "the default", which is the best answer available.
|
|
70
|
+
*
|
|
71
|
+
* @internal — disk resolution.
|
|
68
72
|
*/
|
|
69
73
|
export function defaultDiskName(): string {
|
|
70
74
|
if (_defaultName !== null) return _defaultName;
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test and advanced-wiring seams for `@zerotal/media`.
|
|
3
|
+
*
|
|
4
|
+
* These let a test drive the package without standing up a container: swap the
|
|
5
|
+
* disk resolver, plant a config/driver pair, reset it afterwards. They are the
|
|
6
|
+
* package's own internals, exposed because testing media without them is
|
|
7
|
+
* genuinely awkward — not because they are part of the API.
|
|
8
|
+
*
|
|
9
|
+
* They live behind `@zerotal/media/testing` so the main entry point can be
|
|
10
|
+
* frozen while these stay free to change:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { setDiskResolver, resetMediaState } from "@zerotal/media/testing";
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Nothing here is covered by the package's stability guarantee. If you find
|
|
17
|
+
* yourself needing one of these in application code, that is a gap in the real
|
|
18
|
+
* API worth reporting.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Shared state — the config and driver every operation reads.
|
|
22
|
+
export { mediaState, setMediaState, resetMediaState } from "./state.ts";
|
|
23
|
+
export type { MediaState } from "./state.ts";
|
|
24
|
+
|
|
25
|
+
// Disk resolution — the seam that lets tests skip building a container.
|
|
26
|
+
export { setDiskResolver, setDefaultDiskName, diskNameFor } from "./support/disks.ts";
|
|
27
|
+
export type { DiskResolver } from "./support/disks.ts";
|
|
28
|
+
|
|
29
|
+
// Conversion dispatch — swap the queue for a spy, or force the inline path.
|
|
30
|
+
export { setConversionDispatcher } from "./conversions/dispatch.ts";
|
|
31
|
+
export type { ConversionDispatcher } from "./conversions/dispatch.ts";
|
|
32
|
+
|
|
33
|
+
// Queue-bridge internals, reachable for tests that assert on job behaviour.
|
|
34
|
+
export { performConversions, ownerClassFor } from "./conversions/queueBridge.ts";
|
|
35
|
+
export { partitionConversions } from "./conversions/ConversionRunner.ts";
|
package/src/types.ts
CHANGED
|
@@ -24,8 +24,13 @@ export type ConversionFormat = SafeConversionFormat | "avif" | "heic";
|
|
|
24
24
|
* ratio. The result may be smaller than the box in one dimension.
|
|
25
25
|
* - `fill` — stretch to exactly `width` × `height`, ignoring aspect ratio.
|
|
26
26
|
* - `cover` — scale and centre-crop to exactly fill the box, preserving aspect
|
|
27
|
-
* ratio.
|
|
28
|
-
*
|
|
27
|
+
* ratio. Needs both `width` and `height`: given one, there is nothing to crop
|
|
28
|
+
* away and it behaves as `inside`.
|
|
29
|
+
*
|
|
30
|
+
* With `withoutEnlargement` (the default), a source too small to fill the box is
|
|
31
|
+
* never scaled up — `cover` then yields the largest centre window the source can
|
|
32
|
+
* supply, which may not have the requested aspect ratio. A 300×500 source asked
|
|
33
|
+
* to cover 400×400 gives 300×400.
|
|
29
34
|
*/
|
|
30
35
|
export type ConversionFit = "inside" | "fill" | "cover";
|
|
31
36
|
|
|
@@ -43,6 +48,15 @@ export interface ConversionDefinition {
|
|
|
43
48
|
quality?: number;
|
|
44
49
|
/** Clockwise rotation in degrees applied before resizing. */
|
|
45
50
|
rotate?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Allow scaling a source *up* to meet the target box. Default: `false`.
|
|
53
|
+
*
|
|
54
|
+
* Off by default because upscaling produces a larger file that looks worse.
|
|
55
|
+
* Turn it on when the exact box matters more than fidelity — a `cover`
|
|
56
|
+
* thumbnail for a fixed-size grid slot, say, which would otherwise come back
|
|
57
|
+
* undersized (and off-aspect) for small sources.
|
|
58
|
+
*/
|
|
59
|
+
allowEnlargement?: boolean;
|
|
46
60
|
/** Generate this conversion on the queue instead of inline. Default: `false`. */
|
|
47
61
|
queued?: boolean;
|
|
48
62
|
}
|