@byline/storage-local 1.6.2 → 1.7.1
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.
|
@@ -11,7 +11,14 @@ export interface LocalStorageConfig {
|
|
|
11
11
|
* Absolute or project-relative path to the root directory where uploaded
|
|
12
12
|
* files will be stored.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* For TanStack Start + Nitro hosts, keep this OUTSIDE `public/` — anything
|
|
15
|
+
* under `public/` is snapshotted into `.output/public/` at build time and
|
|
16
|
+
* served by Nitro's static handler from that snapshot, so files written at
|
|
17
|
+
* runtime won't appear until the next rebuild. Pair `uploadDir` with a
|
|
18
|
+
* runtime handler in your server entry that streams from this directory
|
|
19
|
+
* on every request.
|
|
20
|
+
*
|
|
21
|
+
* @example `'./uploads'`
|
|
15
22
|
*/
|
|
16
23
|
uploadDir: string;
|
|
17
24
|
/**
|
|
@@ -30,10 +37,18 @@ export interface LocalStorageConfig {
|
|
|
30
37
|
* `<collection>/<year>/<month>/<uuid>-<filename>`
|
|
31
38
|
*
|
|
32
39
|
* The provider generates a public URL by prepending `baseUrl` to the
|
|
33
|
-
* storage path. Pair this with a
|
|
34
|
-
* `express.static`,
|
|
40
|
+
* storage path. Pair this with a runtime file handler (Express
|
|
41
|
+
* `express.static`, an h3 handler, an nginx location block, or a small
|
|
42
|
+
* `Request → Response` shim in your TanStack Start `server.ts`) that
|
|
35
43
|
* serves the `uploadDir` directory at the `baseUrl` path.
|
|
36
44
|
*
|
|
45
|
+
* On TanStack Start + Nitro, do NOT use `nitro.publicAssets` for this:
|
|
46
|
+
* `publicAssets` copies into `.output/public/<baseURL>/` at build time
|
|
47
|
+
* and the Nitro static handler reads from a build-time virtual asset
|
|
48
|
+
* registry, so files written after the build never resolve. Use a
|
|
49
|
+
* runtime handler in `src/server.ts` instead — see the host scaffold
|
|
50
|
+
* shipped by `@byline/cli` for a worked example.
|
|
51
|
+
*
|
|
37
52
|
* @example
|
|
38
53
|
* ```ts
|
|
39
54
|
* import { localStorageProvider } from '@byline/storage-local'
|
|
@@ -43,7 +58,7 @@ export interface LocalStorageConfig {
|
|
|
43
58
|
* ...config,
|
|
44
59
|
* db: pgAdapter({ connectionString: process.env.DB_CONNECTION_STRING! }),
|
|
45
60
|
* storage: localStorageProvider({
|
|
46
|
-
* uploadDir: './
|
|
61
|
+
* uploadDir: './uploads',
|
|
47
62
|
* baseUrl: '/uploads',
|
|
48
63
|
* }),
|
|
49
64
|
* })
|
|
@@ -97,10 +97,18 @@ class LocalStorageProvider {
|
|
|
97
97
|
* `<collection>/<year>/<month>/<uuid>-<filename>`
|
|
98
98
|
*
|
|
99
99
|
* The provider generates a public URL by prepending `baseUrl` to the
|
|
100
|
-
* storage path. Pair this with a
|
|
101
|
-
* `express.static`,
|
|
100
|
+
* storage path. Pair this with a runtime file handler (Express
|
|
101
|
+
* `express.static`, an h3 handler, an nginx location block, or a small
|
|
102
|
+
* `Request → Response` shim in your TanStack Start `server.ts`) that
|
|
102
103
|
* serves the `uploadDir` directory at the `baseUrl` path.
|
|
103
104
|
*
|
|
105
|
+
* On TanStack Start + Nitro, do NOT use `nitro.publicAssets` for this:
|
|
106
|
+
* `publicAssets` copies into `.output/public/<baseURL>/` at build time
|
|
107
|
+
* and the Nitro static handler reads from a build-time virtual asset
|
|
108
|
+
* registry, so files written after the build never resolve. Use a
|
|
109
|
+
* runtime handler in `src/server.ts` instead — see the host scaffold
|
|
110
|
+
* shipped by `@byline/cli` for a worked example.
|
|
111
|
+
*
|
|
104
112
|
* @example
|
|
105
113
|
* ```ts
|
|
106
114
|
* import { localStorageProvider } from '@byline/storage-local'
|
|
@@ -110,7 +118,7 @@ class LocalStorageProvider {
|
|
|
110
118
|
* ...config,
|
|
111
119
|
* db: pgAdapter({ connectionString: process.env.DB_CONNECTION_STRING! }),
|
|
112
120
|
* storage: localStorageProvider({
|
|
113
|
-
* uploadDir: './
|
|
121
|
+
* uploadDir: './uploads',
|
|
114
122
|
* baseUrl: '/uploads',
|
|
115
123
|
* }),
|
|
116
124
|
* })
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/storage-local",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.7.1",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"npm-run-all": "^4.1.5",
|
|
42
42
|
"uuid": "^14.0.0",
|
|
43
|
-
"@byline/core": "1.
|
|
43
|
+
"@byline/core": "1.7.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@biomejs/biome": "2.4.14",
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
import type { BylineLogger, ImageSize } from '@byline/core';
|
|
9
|
-
export interface ImageMeta {
|
|
10
|
-
width: number | null;
|
|
11
|
-
height: number | null;
|
|
12
|
-
format: string | null;
|
|
13
|
-
}
|
|
14
|
-
export interface ImageVariantResult {
|
|
15
|
-
name: string;
|
|
16
|
-
storagePath: string;
|
|
17
|
-
width: number | undefined;
|
|
18
|
-
height: number | undefined;
|
|
19
|
-
format: string;
|
|
20
|
-
}
|
|
21
|
-
export interface ProcessImageResult {
|
|
22
|
-
meta: ImageMeta;
|
|
23
|
-
variants: ImageVariantResult[];
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Returns true for MIME types that should skip Sharp processing.
|
|
27
|
-
* SVGs are vector files — Sharp cannot meaningfully resize or convert them,
|
|
28
|
-
* and they do not benefit from the responsive-image pipeline.
|
|
29
|
-
*/
|
|
30
|
-
export declare function isBypassMimeType(mimeType: string): boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Extract basic image metadata (dimensions + format) from a buffer using Sharp.
|
|
33
|
-
* Returns nulls for non-image or unrecognised formats.
|
|
34
|
-
*/
|
|
35
|
-
export declare function extractImageMeta(buffer: Buffer, mimeType: string): Promise<ImageMeta>;
|
|
36
|
-
/**
|
|
37
|
-
* Generate the named image variants (sizes) defined in `UploadConfig.sizes`.
|
|
38
|
-
*
|
|
39
|
-
* - SVG and GIF files are skipped entirely (bypass types).
|
|
40
|
-
* - Each variant is written as a sibling file to the original, using the
|
|
41
|
-
* naming convention: `<basename>-<variantName>.<ext>`
|
|
42
|
-
* - Returns an array of `ImageVariantResult` describing what was created.
|
|
43
|
-
*/
|
|
44
|
-
export declare function generateImageVariants(sourceBuffer: Buffer, mimeType: string, absoluteOriginalPath: string, storageBaseDir: string, sizes: ImageSize[], logger?: BylineLogger): Promise<ImageVariantResult[]>;
|
package/dist/image-processor.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
import fs from 'node:fs';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
// Sharp ships its own types; no @types/sharp needed.
|
|
11
|
-
import sharp from 'sharp';
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// SVG detection
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
/**
|
|
16
|
-
* Returns true for MIME types that should skip Sharp processing.
|
|
17
|
-
* SVGs are vector files — Sharp cannot meaningfully resize or convert them,
|
|
18
|
-
* and they do not benefit from the responsive-image pipeline.
|
|
19
|
-
*/
|
|
20
|
-
export function isBypassMimeType(mimeType) {
|
|
21
|
-
return mimeType === 'image/svg+xml' || mimeType === 'image/gif';
|
|
22
|
-
}
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Metadata extraction
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
/**
|
|
27
|
-
* Extract basic image metadata (dimensions + format) from a buffer using Sharp.
|
|
28
|
-
* Returns nulls for non-image or unrecognised formats.
|
|
29
|
-
*/
|
|
30
|
-
export async function extractImageMeta(buffer, mimeType) {
|
|
31
|
-
if (isBypassMimeType(mimeType)) {
|
|
32
|
-
// For SVGs, attempt a lightweight XML parse for width/height attributes.
|
|
33
|
-
const svgMeta = tryParseSvgDimensions(buffer);
|
|
34
|
-
return { width: svgMeta.width, height: svgMeta.height, format: 'svg' };
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const metadata = await sharp(buffer).metadata();
|
|
38
|
-
return {
|
|
39
|
-
width: metadata.width ?? null,
|
|
40
|
-
height: metadata.height ?? null,
|
|
41
|
-
format: metadata.format ?? null,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return { width: null, height: null, format: null };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* A lightweight SVG width/height parser — avoids pulling in a full XML library.
|
|
50
|
-
* Only reads the root `<svg>` element's `width` and `height` attributes.
|
|
51
|
-
*/
|
|
52
|
-
function tryParseSvgDimensions(buffer) {
|
|
53
|
-
try {
|
|
54
|
-
const text = buffer.toString('utf8', 0, Math.min(buffer.length, 2048));
|
|
55
|
-
const widthMatch = text.match(/<svg[^>]*\swidth=["']([0-9.]+)(?:px)?["']/);
|
|
56
|
-
const heightMatch = text.match(/<svg[^>]*\sheight=["']([0-9.]+)(?:px)?["']/);
|
|
57
|
-
return {
|
|
58
|
-
width: widthMatch ? Math.round(Number.parseFloat(widthMatch[1])) : null,
|
|
59
|
-
height: heightMatch ? Math.round(Number.parseFloat(heightMatch[1])) : null,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return { width: null, height: null };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Variant generation
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
/**
|
|
70
|
-
* Generate the named image variants (sizes) defined in `UploadConfig.sizes`.
|
|
71
|
-
*
|
|
72
|
-
* - SVG and GIF files are skipped entirely (bypass types).
|
|
73
|
-
* - Each variant is written as a sibling file to the original, using the
|
|
74
|
-
* naming convention: `<basename>-<variantName>.<ext>`
|
|
75
|
-
* - Returns an array of `ImageVariantResult` describing what was created.
|
|
76
|
-
*/
|
|
77
|
-
export async function generateImageVariants(sourceBuffer, mimeType, absoluteOriginalPath, storageBaseDir, sizes, logger) {
|
|
78
|
-
if (isBypassMimeType(mimeType) || sizes.length === 0) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
const originalExt = path.extname(absoluteOriginalPath);
|
|
82
|
-
const originalBase = path.basename(absoluteOriginalPath, originalExt);
|
|
83
|
-
const variantDir = path.dirname(absoluteOriginalPath);
|
|
84
|
-
const variants = [];
|
|
85
|
-
for (const size of sizes) {
|
|
86
|
-
const outputFormat = size.format ?? 'webp';
|
|
87
|
-
const outputExt = `.${outputFormat}`;
|
|
88
|
-
const variantFilename = `${originalBase}-${size.name}${outputExt}`;
|
|
89
|
-
const variantAbsolutePath = path.join(variantDir, variantFilename);
|
|
90
|
-
// Derive the storage-relative path (relative to storageBaseDir).
|
|
91
|
-
const variantStoragePath = path
|
|
92
|
-
.relative(storageBaseDir, variantAbsolutePath)
|
|
93
|
-
.replace(/\\/g, '/');
|
|
94
|
-
try {
|
|
95
|
-
let pipeline = sharp(sourceBuffer);
|
|
96
|
-
const resizeOptions = {
|
|
97
|
-
width: size.width,
|
|
98
|
-
height: size.height,
|
|
99
|
-
fit: size.fit ?? 'cover',
|
|
100
|
-
withoutEnlargement: true,
|
|
101
|
-
};
|
|
102
|
-
pipeline = pipeline.resize(resizeOptions);
|
|
103
|
-
// Apply format + quality.
|
|
104
|
-
switch (outputFormat) {
|
|
105
|
-
case 'jpeg':
|
|
106
|
-
pipeline = pipeline.jpeg({ quality: size.quality ?? 85 });
|
|
107
|
-
break;
|
|
108
|
-
case 'png':
|
|
109
|
-
pipeline = pipeline.png({ quality: size.quality ?? 85 });
|
|
110
|
-
break;
|
|
111
|
-
case 'webp':
|
|
112
|
-
pipeline = pipeline.webp({ quality: size.quality ?? 85 });
|
|
113
|
-
break;
|
|
114
|
-
case 'avif':
|
|
115
|
-
pipeline = pipeline.avif({ quality: size.quality ?? 55 });
|
|
116
|
-
break;
|
|
117
|
-
default:
|
|
118
|
-
pipeline = pipeline.webp({ quality: size.quality ?? 85 });
|
|
119
|
-
}
|
|
120
|
-
const variantBuffer = await pipeline.toBuffer();
|
|
121
|
-
fs.mkdirSync(path.dirname(variantAbsolutePath), { recursive: true });
|
|
122
|
-
await fs.promises.writeFile(variantAbsolutePath, variantBuffer);
|
|
123
|
-
const sharpMeta = await sharp(variantBuffer).metadata();
|
|
124
|
-
variants.push({
|
|
125
|
-
name: size.name,
|
|
126
|
-
storagePath: variantStoragePath,
|
|
127
|
-
width: sharpMeta.width,
|
|
128
|
-
height: sharpMeta.height,
|
|
129
|
-
format: outputFormat,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
logger?.error({ err, variant: size.name }, 'failed to generate image variant');
|
|
134
|
-
// Non-fatal: skip this variant but continue with others.
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return variants;
|
|
138
|
-
}
|