@nomideusz/svelte-media 0.2.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/CHANGELOG.md +81 -0
- package/README.md +34 -1
- package/dist/components/ImageUpload.svelte +9 -2
- package/dist/components/ImageUpload.svelte.d.ts +1 -0
- package/dist/core/adapter.d.ts +2 -2
- package/dist/core/adapter.js +56 -1
- package/dist/core/local-adapter.js +24 -2
- package/dist/core/media.d.ts +32 -3
- package/dist/core/media.js +55 -3
- package/dist/core/process.d.ts +7 -1
- package/dist/core/process.js +28 -4
- package/dist/core/serve.d.ts +8 -0
- package/dist/core/serve.js +26 -0
- package/dist/core/store.d.ts +30 -0
- package/dist/core/store.js +22 -0
- package/dist/core/types.d.ts +68 -14
- package/dist/core/types.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-08-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **The read half of the pipeline.** `StorageAdapter` gains
|
|
7
|
+
`get(key): Promise<StorageObject | null>` (null = not found; real failures
|
|
8
|
+
reject), implemented by both adapters. Every deployment of this package was
|
|
9
|
+
serving from a private bucket, and both source apps had built parallel raw
|
|
10
|
+
S3 clients — three across the two apps — to work around the adapter being
|
|
11
|
+
write-only.
|
|
12
|
+
- **`getSignedUrl(key, expiresIn?)` on the S3 adapter** (the `S3Adapter`
|
|
13
|
+
type), with expiry-buffered caching and in-flight dedupe — absorbed from
|
|
14
|
+
thebest's hand-rolled presign layer. This is what the previously-dead
|
|
15
|
+
`@aws-sdk/s3-request-presigner` dependency is now for.
|
|
16
|
+
- **`serveMedia(adapter, key): Promise<Response>`** — the whole body of a
|
|
17
|
+
same-origin `/photos/[...key]`-style route: key validation (incl. `..`
|
|
18
|
+
traversal — one app's hand-written copy lacked the check), 404 on missing,
|
|
19
|
+
immutable cache headers.
|
|
20
|
+
- **`createMediaStore({ adapter, ...limits, derive? })`** — the per-app
|
|
21
|
+
binding factory both apps had hand-built (`photo-storage.ts`, `media.ts`),
|
|
22
|
+
returning bound `store/remove/get/serve/url/signedUrl/validate`. Env reading
|
|
23
|
+
stays in the app; the adapter is the seam.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **Breaking for external adapter implementations:** `get` is now a required
|
|
27
|
+
`StorageAdapter` method. (Both in-repo apps consume the built-in adapters.)
|
|
28
|
+
- `S3Config.publicUrl` is now optional — both real deployments serve private
|
|
29
|
+
buckets and were passing dummy values to satisfy the field. `getUrl()`
|
|
30
|
+
throws without it.
|
|
31
|
+
|
|
32
|
+
## 0.4.0 — 2026-08-09
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **`SIZE_PREFIXES` and `SIZE_QUALITY` are now exported** (root and `/server`).
|
|
36
|
+
Both source apps had re-copied the prefix table by hand (one script copied the
|
|
37
|
+
quality values too) because the package kept them private — and the README had
|
|
38
|
+
already drifted from the code once (`lg_` vs `large_`). The variant naming is a
|
|
39
|
+
cross-app contract; now it has one home.
|
|
40
|
+
- **Joined-key helpers** for apps that persist the single original key rather
|
|
41
|
+
than the (prefix, entityId, filename) triple: `variantKey(key, size)`,
|
|
42
|
+
`parseStorageKey(key)` (null for keys that don't fit the layout, e.g. legacy
|
|
43
|
+
ids), and `deleteMediaByKey(adapter, key)` on `/server`. Also
|
|
44
|
+
`sizeForWidth(width?)` — the smallest pre-generated variant that still fills a
|
|
45
|
+
display width (moved in from yoga's hand-rolled copy).
|
|
46
|
+
- **Machine-readable validation.** `ValidationResult` gains
|
|
47
|
+
`code: 'empty' | 'file-too-large' | 'invalid-type'` plus `maxBytes` /
|
|
48
|
+
`allowedTypes`, and `processAndStore` now throws a typed
|
|
49
|
+
`MediaValidationError` carrying them — so a PL/EN/UK app maps `code` to its
|
|
50
|
+
own copy instead of surfacing the English `message`. Both apps had
|
|
51
|
+
re-implemented validation in 4 places purely to localize the strings.
|
|
52
|
+
The `empty` code also covers the zero-byte file every form action was
|
|
53
|
+
checking by hand.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- `ImageUpload`'s declared `config` prop was never read, so the client-side
|
|
57
|
+
pre-validation it implied never ran. It now validates each file before
|
|
58
|
+
`onUpload` and routes failures to `onError`.
|
|
59
|
+
- README documented the large-variant prefix as `lg_`; the code writes
|
|
60
|
+
`large_`.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- `generateMediaKey()` no longer takes the ignored `originalName` parameter.
|
|
64
|
+
- The dead duplicate `DEFAULT_MAX_SIZE`/`DEFAULT_ALLOWED_TYPES` constants and
|
|
65
|
+
the unused `MediaValidation`/`MediaValidationOk` types in `core/types.ts` are
|
|
66
|
+
gone. (`MediaValidationError` is now the thrown error class instead of an
|
|
67
|
+
unused interface — technically a change to `/server`'s type surface.)
|
|
68
|
+
|
|
69
|
+
## 0.3.0 — 2026-08-03
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- **`derive` / `onDeriveError` on `MediaConfig`, and `derived` on `StoredMedia`.**
|
|
73
|
+
A hook over the decoded bytes during processing, so anything computed from the
|
|
74
|
+
image — placeholder, blurhash, dominant colour, EXIF — happens while the bytes
|
|
75
|
+
are in memory rather than fetching them back later. `TDerived` is inferred from
|
|
76
|
+
the callback, so the package takes no dependency on what you derive.
|
|
77
|
+
|
|
78
|
+
The hook runs after every size is stored, and failures deliberately do not
|
|
79
|
+
propagate: by then the upload has succeeded, and rejecting would make the
|
|
80
|
+
caller retry a completed upload and orphan the objects already written.
|
|
81
|
+
`derived` is left undefined and `onDeriveError` is called.
|
|
82
|
+
|
|
83
|
+
|
|
3
84
|
## 0.2.0 — 2026-08-03
|
|
4
85
|
|
|
5
86
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @nomideusz/svelte-media
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@nomideusz/svelte-media) [](https://github.com/nomideusz/svelte-media/blob/main/LICENSE)
|
|
4
|
+
|
|
3
5
|
Image upload, resizing and multi-size storage for Svelte 5 apps. One call takes
|
|
4
6
|
a `File`, validates it, renders four sizes with sharp, and writes them through a
|
|
5
7
|
pluggable storage adapter — S3-compatible or local disk.
|
|
@@ -58,6 +60,32 @@ touches a database.
|
|
|
58
60
|
**Output is always WebP**, whatever was uploaded; `generateMediaKey` names every
|
|
59
61
|
file `<cuid2>.webp` and ignores the original extension.
|
|
60
62
|
|
|
63
|
+
## Deriving extras at upload time
|
|
64
|
+
|
|
65
|
+
`processAndStore` already has the decoded bytes in memory. A `derive` hook lets
|
|
66
|
+
you compute something from them in the same pass — a placeholder, a blurhash, a
|
|
67
|
+
dominant colour, EXIF — instead of fetching the image back later:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { generatePlaceholder } from '@nomideusz/svelte-geometrize/node';
|
|
71
|
+
|
|
72
|
+
const stored = await processAndStore(storage, file, 'tours', tourId, {
|
|
73
|
+
derive: ({ buffer }) => generatePlaceholder(buffer),
|
|
74
|
+
onDeriveError: (err) => log.warn('placeholder failed', err),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
stored.derived; // GeometrizePlaceholder | undefined — inferred from the hook
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The hook runs **after** every size is stored, and **failures do not propagate**:
|
|
81
|
+
by that point the upload has succeeded, and rejecting would make the caller
|
|
82
|
+
retry a completed upload — orphaning the objects already written. A failing
|
|
83
|
+
hook leaves `derived` undefined and calls `onDeriveError`. Wire that to your
|
|
84
|
+
logger, or the failure is silent.
|
|
85
|
+
|
|
86
|
+
The package takes no dependency on whatever you derive; `TDerived` is inferred
|
|
87
|
+
from the callback's return type.
|
|
88
|
+
|
|
61
89
|
## Sizes
|
|
62
90
|
|
|
63
91
|
`processAndStore` always writes four variants:
|
|
@@ -67,7 +95,7 @@ file `<cuid2>.webp` and ignores the original extension.
|
|
|
67
95
|
| `original` | unchanged | — | *(none)* |
|
|
68
96
|
| `thumbnail` | 300×300 | `cover` | `thumb_` |
|
|
69
97
|
| `medium` | 800×600 | `inside` | `med_` |
|
|
70
|
-
| `large` | 1200×900 | `inside` | `
|
|
98
|
+
| `large` | 1200×900 | `inside` | `large_` |
|
|
71
99
|
|
|
72
100
|
`cover` crops to fill; `inside` fits within the box and preserves aspect ratio,
|
|
73
101
|
so `medium` and `large` are upper bounds rather than exact dimensions. The table
|
|
@@ -126,6 +154,11 @@ await deleteMedia(storage, stored.prefix, stored.entityId, stored.filename);
|
|
|
126
154
|
|
|
127
155
|
## Components
|
|
128
156
|
|
|
157
|
+
> **Demo-grade.** Both production apps built on this package hand-roll their
|
|
158
|
+
> upload UI (multi-file queues, form-action submission, brand styling, i18n)
|
|
159
|
+
> and import only the server half. Treat these as starters to copy from, not
|
|
160
|
+
> primitives to build on.
|
|
161
|
+
|
|
129
162
|
```svelte
|
|
130
163
|
<script lang="ts">
|
|
131
164
|
import { ImageUpload, ImageGallery } from '@nomideusz/svelte-media';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts">import { validateImageFile } from "../core/media.js";
|
|
2
|
+
let {
|
|
2
3
|
onUpload,
|
|
3
4
|
onError,
|
|
4
5
|
maxFiles = 1,
|
|
5
|
-
accept = "image/jpeg,image/png,image/webp"
|
|
6
|
+
accept = "image/jpeg,image/png,image/webp",
|
|
7
|
+
config
|
|
6
8
|
} = $props();
|
|
7
9
|
let dragging = $state(false);
|
|
8
10
|
let uploading = $state(false);
|
|
@@ -12,6 +14,11 @@ async function handleFiles(files) {
|
|
|
12
14
|
uploading = true;
|
|
13
15
|
try {
|
|
14
16
|
for (const file of Array.from(files).slice(0, maxFiles)) {
|
|
17
|
+
const validation = validateImageFile(file, config);
|
|
18
|
+
if (!validation.valid) {
|
|
19
|
+
onError?.(validation.error ?? "Invalid file");
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
15
22
|
await onUpload(file);
|
|
16
23
|
}
|
|
17
24
|
} catch (e) {
|
|
@@ -4,6 +4,7 @@ interface Props {
|
|
|
4
4
|
onError?: (message: string) => void;
|
|
5
5
|
maxFiles?: number;
|
|
6
6
|
accept?: string;
|
|
7
|
+
/** Pre-validates each file before onUpload; failures go to onError. */
|
|
7
8
|
config?: MediaConfig;
|
|
8
9
|
}
|
|
9
10
|
declare const ImageUpload: import("svelte").Component<Props, {}, "">;
|
package/dist/core/adapter.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function createS3Adapter(config: S3Config):
|
|
1
|
+
import type { S3Adapter, S3Config } from './types.js';
|
|
2
|
+
export declare function createS3Adapter(config: S3Config): S3Adapter;
|
package/dist/core/adapter.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { S3Client, PutObjectCommand, DeleteObjectCommand, } from '@aws-sdk/client-s3';
|
|
1
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, } from '@aws-sdk/client-s3';
|
|
2
|
+
import { getSignedUrl as presignGetObject } from '@aws-sdk/s3-request-presigner';
|
|
3
|
+
// Refresh signed URLs a minute before they actually expire, so a URL handed to
|
|
4
|
+
// a browser never dies mid-request.
|
|
5
|
+
const SIGNED_TTL_BUFFER_MS = 60_000;
|
|
2
6
|
export function createS3Adapter(config) {
|
|
3
7
|
const client = new S3Client({
|
|
4
8
|
endpoint: config.endpoint,
|
|
@@ -9,6 +13,9 @@ export function createS3Adapter(config) {
|
|
|
9
13
|
},
|
|
10
14
|
forcePathStyle: config.forcePathStyle ?? true,
|
|
11
15
|
});
|
|
16
|
+
// ponytail: unbounded caches — media keys are a finite catalog; add LRU if that changes.
|
|
17
|
+
const signedCache = new Map();
|
|
18
|
+
const signedInflight = new Map();
|
|
12
19
|
return {
|
|
13
20
|
async put(key, buffer, contentType) {
|
|
14
21
|
await client.send(new PutObjectCommand({
|
|
@@ -20,6 +27,27 @@ export function createS3Adapter(config) {
|
|
|
20
27
|
CacheControl: 'public, max-age=31536000, immutable',
|
|
21
28
|
}));
|
|
22
29
|
},
|
|
30
|
+
async get(key) {
|
|
31
|
+
let res;
|
|
32
|
+
try {
|
|
33
|
+
res = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: key }));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const err = e;
|
|
37
|
+
if (err.name === 'NoSuchKey' || err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
const body = res.Body?.transformToWebStream();
|
|
43
|
+
if (!body)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
body,
|
|
47
|
+
contentType: res.ContentType ?? 'application/octet-stream',
|
|
48
|
+
contentLength: res.ContentLength ?? null,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
23
51
|
async delete(key) {
|
|
24
52
|
await client.send(new DeleteObjectCommand({
|
|
25
53
|
Bucket: config.bucket,
|
|
@@ -27,7 +55,34 @@ export function createS3Adapter(config) {
|
|
|
27
55
|
}));
|
|
28
56
|
},
|
|
29
57
|
getUrl(key) {
|
|
58
|
+
if (!config.publicUrl) {
|
|
59
|
+
throw new Error('publicUrl not configured — serve private buckets via get() or getSignedUrl()');
|
|
60
|
+
}
|
|
30
61
|
return `${config.publicUrl.replace(/\/$/, '')}/${key}`;
|
|
31
62
|
},
|
|
63
|
+
async getSignedUrl(key, expiresIn = 3600) {
|
|
64
|
+
const cacheKey = `${expiresIn}:${key}`;
|
|
65
|
+
const cached = signedCache.get(cacheKey);
|
|
66
|
+
if (cached && cached.expiresAt > Date.now())
|
|
67
|
+
return cached.url;
|
|
68
|
+
if (cached)
|
|
69
|
+
signedCache.delete(cacheKey);
|
|
70
|
+
const inflight = signedInflight.get(cacheKey);
|
|
71
|
+
if (inflight)
|
|
72
|
+
return inflight;
|
|
73
|
+
const signing = presignGetObject(client, new GetObjectCommand({ Bucket: config.bucket, Key: key }), { expiresIn })
|
|
74
|
+
.then((url) => {
|
|
75
|
+
signedCache.set(cacheKey, {
|
|
76
|
+
url,
|
|
77
|
+
expiresAt: Date.now() + Math.max(expiresIn * 1000 - SIGNED_TTL_BUFFER_MS, 1_000),
|
|
78
|
+
});
|
|
79
|
+
return url;
|
|
80
|
+
})
|
|
81
|
+
.finally(() => {
|
|
82
|
+
signedInflight.delete(cacheKey);
|
|
83
|
+
});
|
|
84
|
+
signedInflight.set(cacheKey, signing);
|
|
85
|
+
return signing;
|
|
86
|
+
},
|
|
32
87
|
};
|
|
33
88
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { join, dirname } from 'node:path';
|
|
1
|
+
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname, extname } from 'node:path';
|
|
3
|
+
const MIME_BY_EXT = {
|
|
4
|
+
'.webp': 'image/webp',
|
|
5
|
+
'.png': 'image/png',
|
|
6
|
+
'.jpg': 'image/jpeg',
|
|
7
|
+
'.jpeg': 'image/jpeg',
|
|
8
|
+
};
|
|
3
9
|
export function createLocalAdapter(config) {
|
|
4
10
|
return {
|
|
5
11
|
async put(key, buffer, contentType) {
|
|
@@ -7,6 +13,22 @@ export function createLocalAdapter(config) {
|
|
|
7
13
|
await mkdir(dirname(filePath), { recursive: true });
|
|
8
14
|
await writeFile(filePath, buffer);
|
|
9
15
|
},
|
|
16
|
+
async get(key) {
|
|
17
|
+
let buffer;
|
|
18
|
+
try {
|
|
19
|
+
buffer = await readFile(join(config.root, key));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e.code === 'ENOENT')
|
|
23
|
+
return null;
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
body: new Blob([new Uint8Array(buffer)]).stream(),
|
|
28
|
+
contentType: MIME_BY_EXT[extname(key).toLowerCase()] ?? 'application/octet-stream',
|
|
29
|
+
contentLength: buffer.length,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
10
32
|
async delete(key) {
|
|
11
33
|
await unlink(join(config.root, key)).catch(() => { });
|
|
12
34
|
},
|
package/dist/core/media.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageSize,
|
|
1
|
+
import type { ImageSize, ValidationConfig, ValidationErrorCode, ValidationResult } from './types.js';
|
|
2
2
|
export declare const DEFAULT_MAX_SIZE: number;
|
|
3
3
|
export declare const DEFAULT_ALLOWED_TYPES: string[];
|
|
4
4
|
export declare const IMAGE_SIZES: {
|
|
@@ -20,6 +20,35 @@ export declare const IMAGE_SIZES: {
|
|
|
20
20
|
};
|
|
21
21
|
export declare const SIZE_PREFIXES: Record<ImageSize, string>;
|
|
22
22
|
export declare const SIZE_QUALITY: Record<ImageSize, number>;
|
|
23
|
-
export declare function validateImageFile(file: File, config?:
|
|
24
|
-
|
|
23
|
+
export declare function validateImageFile(file: File, config?: ValidationConfig): ValidationResult;
|
|
24
|
+
/**
|
|
25
|
+
* What `processAndStore` throws on a failed validation. Carries the
|
|
26
|
+
* machine-readable `code` (+ limits) so form actions can map it to their own
|
|
27
|
+
* copy instead of surfacing the English `message`.
|
|
28
|
+
*/
|
|
29
|
+
export declare class MediaValidationError extends Error {
|
|
30
|
+
readonly code: ValidationErrorCode;
|
|
31
|
+
readonly maxBytes?: number;
|
|
32
|
+
readonly allowedTypes?: string[];
|
|
33
|
+
constructor(result: ValidationResult);
|
|
34
|
+
}
|
|
35
|
+
export declare function generateMediaKey(): string;
|
|
25
36
|
export declare function getStorageKey(prefix: string, entityId: string, filename: string, size: ImageSize): string;
|
|
37
|
+
/**
|
|
38
|
+
* The variant key for a stored ORIGINAL key (`prefix/entityId/filename`) —
|
|
39
|
+
* for apps that persist the single joined key rather than the
|
|
40
|
+
* (prefix, entityId, filename) triple.
|
|
41
|
+
*/
|
|
42
|
+
export declare function variantKey(key: string, size: ImageSize): string;
|
|
43
|
+
/** The smallest pre-generated variant that still fills `width` display pixels. */
|
|
44
|
+
export declare function sizeForWidth(width?: number): ImageSize;
|
|
45
|
+
/**
|
|
46
|
+
* Inverse of `getStorageKey` for an original key. Returns null for keys that
|
|
47
|
+
* don't fit the `prefix/entityId/filename` layout (e.g. legacy ids), so
|
|
48
|
+
* callers can treat those as "nothing to do".
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseStorageKey(key: string): {
|
|
51
|
+
prefix: string;
|
|
52
|
+
entityId: string;
|
|
53
|
+
filename: string;
|
|
54
|
+
} | null;
|
package/dist/core/media.js
CHANGED
|
@@ -24,16 +24,36 @@ export const SIZE_QUALITY = {
|
|
|
24
24
|
export function validateImageFile(file, config) {
|
|
25
25
|
const maxSize = config?.maxFileSize ?? DEFAULT_MAX_SIZE;
|
|
26
26
|
const allowed = config?.allowedTypes ?? DEFAULT_ALLOWED_TYPES;
|
|
27
|
+
if (file.size === 0) {
|
|
28
|
+
return { valid: false, code: 'empty', error: 'No file provided' };
|
|
29
|
+
}
|
|
27
30
|
if (file.size > maxSize) {
|
|
28
31
|
const mb = Math.round(maxSize / 1024 / 1024);
|
|
29
|
-
return { valid: false, error: `File too large (max ${mb}MB)` };
|
|
32
|
+
return { valid: false, code: 'file-too-large', maxBytes: maxSize, error: `File too large (max ${mb}MB)` };
|
|
30
33
|
}
|
|
31
34
|
if (!allowed.includes(file.type)) {
|
|
32
|
-
return { valid: false, error: `Invalid file type. Allowed: JPEG, PNG, WebP` };
|
|
35
|
+
return { valid: false, code: 'invalid-type', allowedTypes: allowed, error: `Invalid file type. Allowed: JPEG, PNG, WebP` };
|
|
33
36
|
}
|
|
34
37
|
return { valid: true };
|
|
35
38
|
}
|
|
36
|
-
|
|
39
|
+
/**
|
|
40
|
+
* What `processAndStore` throws on a failed validation. Carries the
|
|
41
|
+
* machine-readable `code` (+ limits) so form actions can map it to their own
|
|
42
|
+
* copy instead of surfacing the English `message`.
|
|
43
|
+
*/
|
|
44
|
+
export class MediaValidationError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
maxBytes;
|
|
47
|
+
allowedTypes;
|
|
48
|
+
constructor(result) {
|
|
49
|
+
super(result.error ?? 'Invalid file');
|
|
50
|
+
this.name = 'MediaValidationError';
|
|
51
|
+
this.code = result.code ?? 'invalid-type';
|
|
52
|
+
this.maxBytes = result.maxBytes;
|
|
53
|
+
this.allowedTypes = result.allowedTypes;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function generateMediaKey() {
|
|
37
57
|
// All processed output is WebP regardless of the uploaded format.
|
|
38
58
|
return `${createId()}.webp`;
|
|
39
59
|
}
|
|
@@ -41,3 +61,35 @@ export function getStorageKey(prefix, entityId, filename, size) {
|
|
|
41
61
|
const sizePrefix = SIZE_PREFIXES[size];
|
|
42
62
|
return `${prefix}/${entityId}/${sizePrefix}${filename}`;
|
|
43
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The variant key for a stored ORIGINAL key (`prefix/entityId/filename`) —
|
|
66
|
+
* for apps that persist the single joined key rather than the
|
|
67
|
+
* (prefix, entityId, filename) triple.
|
|
68
|
+
*/
|
|
69
|
+
export function variantKey(key, size) {
|
|
70
|
+
const idx = key.lastIndexOf('/');
|
|
71
|
+
return `${key.slice(0, idx + 1)}${SIZE_PREFIXES[size]}${key.slice(idx + 1)}`;
|
|
72
|
+
}
|
|
73
|
+
/** The smallest pre-generated variant that still fills `width` display pixels. */
|
|
74
|
+
export function sizeForWidth(width) {
|
|
75
|
+
if (!width)
|
|
76
|
+
return 'medium';
|
|
77
|
+
if (width <= IMAGE_SIZES.thumbnail.width)
|
|
78
|
+
return 'thumbnail';
|
|
79
|
+
if (width <= IMAGE_SIZES.medium.width)
|
|
80
|
+
return 'medium';
|
|
81
|
+
return 'large';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Inverse of `getStorageKey` for an original key. Returns null for keys that
|
|
85
|
+
* don't fit the `prefix/entityId/filename` layout (e.g. legacy ids), so
|
|
86
|
+
* callers can treat those as "nothing to do".
|
|
87
|
+
*/
|
|
88
|
+
export function parseStorageKey(key) {
|
|
89
|
+
const parts = key.split('/');
|
|
90
|
+
if (parts.length < 3)
|
|
91
|
+
return null;
|
|
92
|
+
const filename = parts.pop();
|
|
93
|
+
const entityId = parts.pop();
|
|
94
|
+
return { prefix: parts.join('/'), entityId, filename };
|
|
95
|
+
}
|
package/dist/core/process.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { StorageAdapter, StoredMedia, ImageSize, MediaConfig } from './types.js';
|
|
2
|
-
export declare function processAndStore(adapter: StorageAdapter, file: File, prefix: string, entityId: string, config?: MediaConfig): Promise<StoredMedia
|
|
2
|
+
export declare function processAndStore<TDerived = never>(adapter: StorageAdapter, file: File, prefix: string, entityId: string, config?: MediaConfig<TDerived>): Promise<StoredMedia<TDerived>>;
|
|
3
3
|
export declare function deleteMedia(adapter: StorageAdapter, prefix: string, entityId: string, filename: string): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* `deleteMedia` for apps that persist the single joined original key.
|
|
6
|
+
* Keys that don't parse (legacy ids from a previous storage system) are a
|
|
7
|
+
* silent no-op — there is nothing in object storage to delete.
|
|
8
|
+
*/
|
|
9
|
+
export declare function deleteMediaByKey(adapter: StorageAdapter, key: string): Promise<void>;
|
|
4
10
|
export declare function getMediaUrl(adapter: StorageAdapter, prefix: string, entityId: string, filename: string, size?: ImageSize): string;
|
package/dist/core/process.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// browser bundle will fail — the client-safe helpers live in core/media.ts and
|
|
3
3
|
// are re-exported from the package root.
|
|
4
4
|
import sharp from 'sharp';
|
|
5
|
-
import { IMAGE_SIZES, SIZE_QUALITY, generateMediaKey, getStorageKey, validateImageFile, } from './media.js';
|
|
5
|
+
import { IMAGE_SIZES, SIZE_QUALITY, MediaValidationError, generateMediaKey, getStorageKey, parseStorageKey, validateImageFile, } from './media.js';
|
|
6
6
|
export async function processAndStore(adapter, file, prefix, entityId, config) {
|
|
7
7
|
const validation = validateImageFile(file, config);
|
|
8
8
|
if (!validation.valid)
|
|
9
|
-
throw new
|
|
10
|
-
const filename = generateMediaKey(
|
|
9
|
+
throw new MediaValidationError(validation);
|
|
10
|
+
const filename = generateMediaKey();
|
|
11
11
|
const arrayBuffer = await file.arrayBuffer();
|
|
12
12
|
const buffer = Buffer.from(arrayBuffer);
|
|
13
13
|
const sizes = {
|
|
@@ -29,12 +29,36 @@ export async function processAndStore(adapter, file, prefix, entityId, config) {
|
|
|
29
29
|
}
|
|
30
30
|
// ponytail: existing objects stay JPEG under their old keys — URLs derive
|
|
31
31
|
// from the stored filename, so old rows keep working without migration.
|
|
32
|
-
|
|
32
|
+
// Runs last, so a throwing hook cannot orphan a half-written upload — and it
|
|
33
|
+
// is caught, because by this point the image is stored and rejecting would
|
|
34
|
+
// make the caller retry a completed upload.
|
|
35
|
+
let derived;
|
|
36
|
+
if (config?.derive) {
|
|
37
|
+
const source = { buffer, filename, mimeType: file.type };
|
|
38
|
+
try {
|
|
39
|
+
derived = await config.derive(source);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
config.onDeriveError?.(error, source);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { filename, originalName: file.name, prefix, entityId, sizes, derived };
|
|
33
46
|
}
|
|
34
47
|
export async function deleteMedia(adapter, prefix, entityId, filename) {
|
|
35
48
|
const keys = ['original', 'thumbnail', 'medium', 'large'].map((size) => getStorageKey(prefix, entityId, filename, size));
|
|
36
49
|
await Promise.all(keys.map((key) => adapter.delete(key).catch(() => { })));
|
|
37
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* `deleteMedia` for apps that persist the single joined original key.
|
|
53
|
+
* Keys that don't parse (legacy ids from a previous storage system) are a
|
|
54
|
+
* silent no-op — there is nothing in object storage to delete.
|
|
55
|
+
*/
|
|
56
|
+
export async function deleteMediaByKey(adapter, key) {
|
|
57
|
+
const parsed = parseStorageKey(key);
|
|
58
|
+
if (!parsed)
|
|
59
|
+
return;
|
|
60
|
+
await deleteMedia(adapter, parsed.prefix, parsed.entityId, parsed.filename);
|
|
61
|
+
}
|
|
38
62
|
export function getMediaUrl(adapter, prefix, entityId, filename, size = 'medium') {
|
|
39
63
|
return adapter.getUrl(getStorageKey(prefix, entityId, filename, size));
|
|
40
64
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StorageAdapter } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Same-origin media serving: the whole body of a
|
|
4
|
+
* `/photos/[...key]`-style route. Key validation, 404 on missing, immutable
|
|
5
|
+
* cache headers (keys are content-addressed and never change in place).
|
|
6
|
+
* Returns a standard `Response` — return it directly from a SvelteKit handler.
|
|
7
|
+
*/
|
|
8
|
+
export declare function serveMedia(adapter: StorageAdapter, key: string): Promise<Response>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Media keys are `prefix/entityId/(sizePrefix)filename.ext` — reject anything
|
|
2
|
+
// else (especially `..` traversal) before it reaches the adapter. Both source
|
|
3
|
+
// apps had written this route by hand; the copy without the guard had a path
|
|
4
|
+
// traversal, which is why the validation lives here now.
|
|
5
|
+
const MEDIA_KEY = /^[a-zA-Z0-9_/.-]+\.(webp|jpe?g|png)$/;
|
|
6
|
+
/**
|
|
7
|
+
* Same-origin media serving: the whole body of a
|
|
8
|
+
* `/photos/[...key]`-style route. Key validation, 404 on missing, immutable
|
|
9
|
+
* cache headers (keys are content-addressed and never change in place).
|
|
10
|
+
* Returns a standard `Response` — return it directly from a SvelteKit handler.
|
|
11
|
+
*/
|
|
12
|
+
export async function serveMedia(adapter, key) {
|
|
13
|
+
if (!MEDIA_KEY.test(key) || key.includes('..')) {
|
|
14
|
+
return new Response('Not found', { status: 404 });
|
|
15
|
+
}
|
|
16
|
+
const obj = await adapter.get(key);
|
|
17
|
+
if (!obj)
|
|
18
|
+
return new Response('Not found', { status: 404 });
|
|
19
|
+
return new Response(obj.body, {
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': obj.contentType,
|
|
22
|
+
...(obj.contentLength != null ? { 'Content-Length': String(obj.contentLength) } : {}),
|
|
23
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ImageSize, MediaConfig, StorageAdapter, StorageObject, StoredMedia, ValidationResult } from './types.js';
|
|
2
|
+
export interface MediaStoreConfig<TDerived = never> extends MediaConfig<TDerived> {
|
|
3
|
+
adapter: StorageAdapter;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The per-app binding — adapter, limits and derive hook bound once, so call
|
|
7
|
+
* sites pass only what varies per upload. Both source apps had hand-built this
|
|
8
|
+
* module (and one ended up configuring the same bucket two different ways);
|
|
9
|
+
* app #3 starts here instead.
|
|
10
|
+
*
|
|
11
|
+
* Env reading stays in the app: build the adapter from your own env names and
|
|
12
|
+
* hand it in.
|
|
13
|
+
*/
|
|
14
|
+
export interface MediaStore<TDerived = never> {
|
|
15
|
+
adapter: StorageAdapter;
|
|
16
|
+
/** `processAndStore` with the bound adapter + config. */
|
|
17
|
+
store(file: File, prefix: string, entityId: string): Promise<StoredMedia<TDerived>>;
|
|
18
|
+
/** Deletes all variants by the joined original key; unparseable keys are a no-op. */
|
|
19
|
+
remove(key: string): Promise<void>;
|
|
20
|
+
get(key: string): Promise<StorageObject | null>;
|
|
21
|
+
/** `serveMedia` with the bound adapter — a complete same-origin route body. */
|
|
22
|
+
serve(key: string): Promise<Response>;
|
|
23
|
+
/** Public URL for a size variant (adapters with a publicUrl only). */
|
|
24
|
+
url(key: string, size?: ImageSize): string;
|
|
25
|
+
/** Presigned URL for a size variant — rejects unless the adapter is S3. */
|
|
26
|
+
signedUrl(key: string, size?: ImageSize, expiresIn?: number): Promise<string>;
|
|
27
|
+
/** `validateImageFile` with the bound limits. */
|
|
28
|
+
validate(file: File): ValidationResult;
|
|
29
|
+
}
|
|
30
|
+
export declare function createMediaStore<TDerived = never>(config: MediaStoreConfig<TDerived>): MediaStore<TDerived>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { validateImageFile, variantKey } from './media.js';
|
|
2
|
+
import { processAndStore, deleteMediaByKey } from './process.js';
|
|
3
|
+
import { serveMedia } from './serve.js';
|
|
4
|
+
export function createMediaStore(config) {
|
|
5
|
+
const { adapter, ...media } = config;
|
|
6
|
+
return {
|
|
7
|
+
adapter,
|
|
8
|
+
store: (file, prefix, entityId) => processAndStore(adapter, file, prefix, entityId, media),
|
|
9
|
+
remove: (key) => deleteMediaByKey(adapter, key),
|
|
10
|
+
get: (key) => adapter.get(key),
|
|
11
|
+
serve: (key) => serveMedia(adapter, key),
|
|
12
|
+
url: (key, size = 'original') => adapter.getUrl(variantKey(key, size)),
|
|
13
|
+
signedUrl: (key, size = 'original', expiresIn) => {
|
|
14
|
+
const s3 = adapter;
|
|
15
|
+
if (!s3.getSignedUrl) {
|
|
16
|
+
return Promise.reject(new Error('signedUrl needs an S3 adapter (getSignedUrl)'));
|
|
17
|
+
}
|
|
18
|
+
return s3.getSignedUrl(variantKey(key, size), expiresIn);
|
|
19
|
+
},
|
|
20
|
+
validate: (file) => validateImageFile(file, media),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
export type ImageSize = 'original' | 'thumbnail' | 'medium' | 'large';
|
|
2
|
+
/** What `StorageAdapter.get` resolves for an existing object. */
|
|
3
|
+
export interface StorageObject {
|
|
4
|
+
body: ReadableStream<Uint8Array>;
|
|
5
|
+
contentType: string;
|
|
6
|
+
contentLength: number | null;
|
|
7
|
+
}
|
|
2
8
|
export interface StorageAdapter {
|
|
3
9
|
put(key: string, buffer: Buffer, contentType: string): Promise<void>;
|
|
10
|
+
/** null when the object doesn't exist; rejects on real failures (network, auth). */
|
|
11
|
+
get(key: string): Promise<StorageObject | null>;
|
|
4
12
|
delete(key: string): Promise<void>;
|
|
13
|
+
/** Public-URL serving only — throws when the adapter has no public base URL. */
|
|
5
14
|
getUrl(key: string): string;
|
|
6
15
|
}
|
|
16
|
+
/** What `createS3Adapter` actually returns — a StorageAdapter that can presign. */
|
|
17
|
+
export interface S3Adapter extends StorageAdapter {
|
|
18
|
+
/**
|
|
19
|
+
* Presigned GET URL for a private bucket. Cached until shortly before
|
|
20
|
+
* expiry and deduped in flight, so page loads with many images cost one
|
|
21
|
+
* signing pass per key.
|
|
22
|
+
*/
|
|
23
|
+
getSignedUrl(key: string, expiresIn?: number): Promise<string>;
|
|
24
|
+
}
|
|
7
25
|
export interface S3Config {
|
|
8
26
|
/** Full endpoint URL, e.g. https://xxx.r2.cloudflarestorage.com */
|
|
9
27
|
endpoint: string;
|
|
@@ -12,12 +30,16 @@ export interface S3Config {
|
|
|
12
30
|
bucket: string;
|
|
13
31
|
accessKeyId: string;
|
|
14
32
|
secretAccessKey: string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Base public URL for getUrl(), e.g. https://pub-xxx.r2.dev. Optional —
|
|
35
|
+
* private buckets serve via `get()` (same-origin proxy) or `getSignedUrl()`
|
|
36
|
+
* instead, and `getUrl()` throws without it.
|
|
37
|
+
*/
|
|
38
|
+
publicUrl?: string;
|
|
17
39
|
/** Use path-style access (default: true for MinIO/R2, set false for Railway/AWS) */
|
|
18
40
|
forcePathStyle?: boolean;
|
|
19
41
|
}
|
|
20
|
-
export interface StoredMedia {
|
|
42
|
+
export interface StoredMedia<TDerived = never> {
|
|
21
43
|
filename: string;
|
|
22
44
|
originalName: string;
|
|
23
45
|
/** Storage prefix, e.g. 'tours' or 'avatars' */
|
|
@@ -26,22 +48,54 @@ export interface StoredMedia {
|
|
|
26
48
|
entityId: string;
|
|
27
49
|
/** Storage keys for each size */
|
|
28
50
|
sizes: Record<ImageSize, string>;
|
|
51
|
+
/**
|
|
52
|
+
* Result of `config.derive`, when supplied. Undefined if no hook was given,
|
|
53
|
+
* or if it threw — see `MediaConfig.onDeriveError`.
|
|
54
|
+
*/
|
|
55
|
+
derived?: TDerived;
|
|
29
56
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
error: string;
|
|
33
|
-
}
|
|
34
|
-
export interface MediaValidationOk {
|
|
35
|
-
isValid: true;
|
|
36
|
-
}
|
|
37
|
-
export type MediaValidation = MediaValidationOk | MediaValidationError;
|
|
38
|
-
export declare const DEFAULT_MAX_SIZE: number;
|
|
39
|
-
export declare const DEFAULT_ALLOWED_TYPES: string[];
|
|
57
|
+
/** Machine-readable validation failure — map it to your own copy/i18n. */
|
|
58
|
+
export type ValidationErrorCode = 'empty' | 'file-too-large' | 'invalid-type';
|
|
40
59
|
export interface ValidationResult {
|
|
41
60
|
valid: boolean;
|
|
61
|
+
/** Set when `valid` is false. */
|
|
62
|
+
code?: ValidationErrorCode;
|
|
63
|
+
/** The limit that produced a 'file-too-large', for message interpolation. */
|
|
64
|
+
maxBytes?: number;
|
|
65
|
+
/** The allow-list that produced an 'invalid-type'. */
|
|
66
|
+
allowedTypes?: string[];
|
|
67
|
+
/** Human-readable English fallback; apps with i18n should map `code` instead. */
|
|
42
68
|
error?: string;
|
|
43
69
|
}
|
|
44
|
-
|
|
70
|
+
/** What a `derive` hook receives — the image as uploaded, before any resizing. */
|
|
71
|
+
export interface DeriveSource {
|
|
72
|
+
/** Original bytes, pre-resize, EXIF intact. */
|
|
73
|
+
buffer: Buffer;
|
|
74
|
+
/** The generated storage filename, e.g. `k7x2m9.webp`. */
|
|
75
|
+
filename: string;
|
|
76
|
+
/** The uploaded file's MIME type. */
|
|
77
|
+
mimeType: string;
|
|
78
|
+
}
|
|
79
|
+
/** The validation half of MediaConfig — all `validateImageFile` needs. */
|
|
80
|
+
export interface ValidationConfig {
|
|
45
81
|
maxFileSize?: number;
|
|
46
82
|
allowedTypes?: string[];
|
|
47
83
|
}
|
|
84
|
+
export interface MediaConfig<TDerived = never> extends ValidationConfig {
|
|
85
|
+
/**
|
|
86
|
+
* Optional side-computation over the image, run once during processing while
|
|
87
|
+
* the bytes are already in hand — a placeholder, a blurhash, a dominant
|
|
88
|
+
* colour, EXIF. Whatever it returns is attached to the result as `derived`.
|
|
89
|
+
*
|
|
90
|
+
* Runs after every size has been stored, so a failure cannot orphan a
|
|
91
|
+
* half-written upload. Failures do not propagate: the upload has already
|
|
92
|
+
* succeeded and a derived extra must never invalidate it. `derived` is left
|
|
93
|
+
* undefined and `onDeriveError` is called.
|
|
94
|
+
*/
|
|
95
|
+
derive?(source: DeriveSource): Promise<TDerived>;
|
|
96
|
+
/**
|
|
97
|
+
* Called when `derive` throws. The upload still succeeds. Without this the
|
|
98
|
+
* failure is silent, so wire it to your logger.
|
|
99
|
+
*/
|
|
100
|
+
onDeriveError?(error: unknown, source: DeriveSource): void;
|
|
101
|
+
}
|
package/dist/core/types.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { StorageAdapter, S3Config, StoredMedia, ImageSize, MediaConfig, ValidationResult, } from './core/types.js';
|
|
2
|
-
export { validateImageFile, generateMediaKey, getStorageKey, IMAGE_SIZES, DEFAULT_MAX_SIZE, DEFAULT_ALLOWED_TYPES, } from './core/media.js';
|
|
1
|
+
export type { StorageAdapter, S3Adapter, S3Config, StorageObject, StoredMedia, ImageSize, MediaConfig, ValidationResult, ValidationConfig, ValidationErrorCode, DeriveSource, } from './core/types.js';
|
|
2
|
+
export { validateImageFile, MediaValidationError, generateMediaKey, getStorageKey, variantKey, sizeForWidth, parseStorageKey, IMAGE_SIZES, SIZE_PREFIXES, SIZE_QUALITY, DEFAULT_MAX_SIZE, DEFAULT_ALLOWED_TYPES, } from './core/media.js';
|
|
3
3
|
export { default as ImageUpload } from './components/ImageUpload.svelte';
|
|
4
4
|
export { default as ImageGallery } from './components/ImageGallery.svelte';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Client-safe entry. The image pipeline and storage adapters need sharp,
|
|
2
2
|
// node:fs and Buffer, so they live behind '@nomideusz/svelte-media/server' —
|
|
3
3
|
// importing them here would break any browser bundle that touches a component.
|
|
4
|
-
export { validateImageFile, generateMediaKey, getStorageKey, IMAGE_SIZES, DEFAULT_MAX_SIZE, DEFAULT_ALLOWED_TYPES, } from './core/media.js';
|
|
4
|
+
export { validateImageFile, MediaValidationError, generateMediaKey, getStorageKey, variantKey, sizeForWidth, parseStorageKey, IMAGE_SIZES, SIZE_PREFIXES, SIZE_QUALITY, DEFAULT_MAX_SIZE, DEFAULT_ALLOWED_TYPES, } from './core/media.js';
|
|
5
5
|
export { default as ImageUpload } from './components/ImageUpload.svelte';
|
|
6
6
|
export { default as ImageGallery } from './components/ImageGallery.svelte';
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type { LocalConfig } from '../core/local-adapter.js';
|
|
2
2
|
export { createS3Adapter } from '../core/adapter.js';
|
|
3
3
|
export { createLocalAdapter } from '../core/local-adapter.js';
|
|
4
|
-
export { processAndStore, deleteMedia, getMediaUrl } from '../core/process.js';
|
|
5
|
-
export {
|
|
4
|
+
export { processAndStore, deleteMedia, deleteMediaByKey, getMediaUrl } from '../core/process.js';
|
|
5
|
+
export { serveMedia } from '../core/serve.js';
|
|
6
|
+
export { createMediaStore } from '../core/store.js';
|
|
7
|
+
export type { MediaStore, MediaStoreConfig } from '../core/store.js';
|
|
8
|
+
export { validateImageFile, MediaValidationError, generateMediaKey, getStorageKey, variantKey, sizeForWidth, parseStorageKey, IMAGE_SIZES, SIZE_PREFIXES, SIZE_QUALITY, } from '../core/media.js';
|
|
6
9
|
export type * from '../core/types.js';
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Server-only entry: sharp, node:fs and Buffer live behind here.
|
|
2
2
|
export { createS3Adapter } from '../core/adapter.js';
|
|
3
3
|
export { createLocalAdapter } from '../core/local-adapter.js';
|
|
4
|
-
export { processAndStore, deleteMedia, getMediaUrl } from '../core/process.js';
|
|
4
|
+
export { processAndStore, deleteMedia, deleteMediaByKey, getMediaUrl } from '../core/process.js';
|
|
5
|
+
export { serveMedia } from '../core/serve.js';
|
|
6
|
+
export { createMediaStore } from '../core/store.js';
|
|
5
7
|
// Re-exported so server code needs only one import.
|
|
6
|
-
export { validateImageFile, generateMediaKey, getStorageKey, IMAGE_SIZES, } from '../core/media.js';
|
|
8
|
+
export { validateImageFile, MediaValidationError, generateMediaKey, getStorageKey, variantKey, sizeForWidth, parseStorageKey, IMAGE_SIZES, SIZE_PREFIXES, SIZE_QUALITY, } from '../core/media.js';
|