@byline/core 1.2.1 → 1.3.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.
@@ -38,6 +38,21 @@ export interface UploadFileOptions {
38
38
  * Providers may use this to namespace storage paths.
39
39
  */
40
40
  collection?: string;
41
+ /**
42
+ * Explicit, fully-qualified storage path / object key the provider must
43
+ * write to verbatim. When set, providers MUST place the file at exactly
44
+ * this path — no UUID prefix, year/month rewrite, or `pathPrefix` injection.
45
+ *
46
+ * Used by the image-variant pipeline (`generateImageVariants`) to write
47
+ * sibling files alongside an already-stored original (e.g. so the
48
+ * `thumbnail` variant of `media/2026/05/abc-photo.jpg` lands at
49
+ * `media/2026/05/abc-photo-thumbnail.webp`).
50
+ *
51
+ * Always POSIX-style (forward slashes), no leading slash. Callers are
52
+ * responsible for sanitisation and collision-avoidance — providers do
53
+ * not second-guess the path when this is set.
54
+ */
55
+ targetStoragePath?: string;
41
56
  }
42
57
  /**
43
58
  * The pluggable file-storage interface.
@@ -6,6 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { ImageSize } from '../@types/collection-types.js';
9
+ import type { IStorageProvider, StoredFileLocation } from '../@types/storage-types.js';
9
10
  import type { BylineLogger } from '../logger/index.js';
10
11
  export interface ImageMeta {
11
12
  width: number | null;
@@ -35,11 +36,16 @@ export declare function isBypassMimeType(mimeType: string): boolean;
35
36
  */
36
37
  export declare function extractImageMeta(buffer: Buffer, mimeType: string): Promise<ImageMeta>;
37
38
  /**
38
- * Generate the named image variants (sizes) defined in `UploadConfig.sizes`.
39
+ * Generate the named image variants (sizes) defined in `UploadConfig.sizes`
40
+ * and persist them through the configured `IStorageProvider`.
39
41
  *
40
42
  * - SVG and GIF files are skipped entirely (bypass types).
41
- * - Each variant is written as a sibling file to the original, using the
42
- * naming convention: `<basename>-<variantName>.<ext>`
43
+ * - Each variant is uploaded as a sibling object to `storedFile.storagePath`,
44
+ * using the naming convention: `<dir>/<basename>-<variantName>.<ext>`
45
+ * (POSIX paths — both local filesystem keys and S3 object keys).
46
+ * - Variant bytes are produced in-memory by Sharp and written via
47
+ * `storage.upload(buffer, { targetStoragePath })` — no direct filesystem
48
+ * access, so the function is provider-agnostic.
43
49
  * - Returns an array of `ImageVariantResult` describing what was created.
44
50
  */
45
- export declare function generateImageVariants(sourceBuffer: Buffer, mimeType: string, absoluteOriginalPath: string, storageBaseDir: string, sizes: ImageSize[], logger?: BylineLogger): Promise<ImageVariantResult[]>;
51
+ export declare function generateImageVariants(sourceBuffer: Buffer, mimeType: string, storedFile: StoredFileLocation, storage: IStorageProvider, sizes: ImageSize[], logger?: BylineLogger): Promise<ImageVariantResult[]>;
@@ -5,7 +5,6 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import fs from 'node:fs';
9
8
  import path from 'node:path';
10
9
  // Sharp ships its own types; no @types/sharp needed.
11
10
  import sharp from 'sharp';
@@ -67,30 +66,52 @@ function tryParseSvgDimensions(buffer) {
67
66
  // Variant generation
68
67
  // ---------------------------------------------------------------------------
69
68
  /**
70
- * Generate the named image variants (sizes) defined in `UploadConfig.sizes`.
69
+ * Map a Sharp output format keyword to the corresponding image MIME type.
70
+ * Unknown formats fall back to `application/octet-stream` — the storage
71
+ * provider will accept it; only the served `Content-Type` is affected.
72
+ */
73
+ function mimeTypeForFormat(format) {
74
+ switch (format) {
75
+ case 'jpeg':
76
+ return 'image/jpeg';
77
+ case 'png':
78
+ return 'image/png';
79
+ case 'webp':
80
+ return 'image/webp';
81
+ case 'avif':
82
+ return 'image/avif';
83
+ default:
84
+ return 'application/octet-stream';
85
+ }
86
+ }
87
+ /**
88
+ * Generate the named image variants (sizes) defined in `UploadConfig.sizes`
89
+ * and persist them through the configured `IStorageProvider`.
71
90
  *
72
91
  * - 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>`
92
+ * - Each variant is uploaded as a sibling object to `storedFile.storagePath`,
93
+ * using the naming convention: `<dir>/<basename>-<variantName>.<ext>`
94
+ * (POSIX paths — both local filesystem keys and S3 object keys).
95
+ * - Variant bytes are produced in-memory by Sharp and written via
96
+ * `storage.upload(buffer, { targetStoragePath })` — no direct filesystem
97
+ * access, so the function is provider-agnostic.
75
98
  * - Returns an array of `ImageVariantResult` describing what was created.
76
99
  */
77
- export async function generateImageVariants(sourceBuffer, mimeType, absoluteOriginalPath, storageBaseDir, sizes, logger) {
100
+ export async function generateImageVariants(sourceBuffer, mimeType, storedFile, storage, sizes, logger) {
78
101
  if (isBypassMimeType(mimeType) || sizes.length === 0) {
79
102
  return [];
80
103
  }
81
- const originalExt = path.extname(absoluteOriginalPath);
82
- const originalBase = path.basename(absoluteOriginalPath, originalExt);
83
- const variantDir = path.dirname(absoluteOriginalPath);
104
+ // Storage paths are POSIX-style across providers; use `path.posix` so
105
+ // Windows local-fs hosts don't introduce backslashes into S3 object keys.
106
+ const sourcePath = storedFile.storagePath;
107
+ const originalExt = path.posix.extname(sourcePath);
108
+ const originalBase = path.posix.basename(sourcePath, originalExt);
109
+ const variantDir = path.posix.dirname(sourcePath);
84
110
  const variants = [];
85
111
  for (const size of sizes) {
86
112
  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, '/');
113
+ const variantFilename = `${originalBase}-${size.name}.${outputFormat}`;
114
+ const variantStoragePath = variantDir === '.' ? variantFilename : `${variantDir}/${variantFilename}`;
94
115
  try {
95
116
  let pipeline = sharp(sourceBuffer);
96
117
  const resizeOptions = {
@@ -118,9 +139,13 @@ export async function generateImageVariants(sourceBuffer, mimeType, absoluteOrig
118
139
  pipeline = pipeline.webp({ quality: size.quality ?? 85 });
119
140
  }
120
141
  const variantBuffer = await pipeline.toBuffer();
121
- fs.mkdirSync(path.dirname(variantAbsolutePath), { recursive: true });
122
- await fs.promises.writeFile(variantAbsolutePath, variantBuffer);
123
142
  const sharpMeta = await sharp(variantBuffer).metadata();
143
+ await storage.upload(variantBuffer, {
144
+ filename: variantFilename,
145
+ mimeType: mimeTypeForFormat(outputFormat),
146
+ size: variantBuffer.byteLength,
147
+ targetStoragePath: variantStoragePath,
148
+ });
124
149
  variants.push({
125
150
  name: size.name,
126
151
  storagePath: variantStoragePath,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/core",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "1.2.1",
5
+ "version": "1.3.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -79,7 +79,7 @@
79
79
  "sharp": "^0.34.5",
80
80
  "uuid": "^14.0.0",
81
81
  "zod": "^4.4.2",
82
- "@byline/auth": "1.2.1"
82
+ "@byline/auth": "1.3.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@biomejs/biome": "2.4.14",