@valencets/cms 0.5.0 → 0.6.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.
Files changed (109) hide show
  1. package/dist/admin/admin-routes.d.ts +2 -0
  2. package/dist/admin/admin-routes.d.ts.map +1 -1
  3. package/dist/admin/admin-routes.js +7 -5
  4. package/dist/admin/admin-routes.js.map +1 -1
  5. package/dist/admin/edit-view.d.ts +9 -1
  6. package/dist/admin/edit-view.d.ts.map +1 -1
  7. package/dist/admin/edit-view.js +105 -35
  8. package/dist/admin/edit-view.js.map +1 -1
  9. package/dist/admin/editor/admin-client.js +1 -1
  10. package/dist/admin/editor/admin-client.js.map +1 -1
  11. package/dist/admin/field-renderers.d.ts +13 -1
  12. package/dist/admin/field-renderers.d.ts.map +1 -1
  13. package/dist/admin/field-renderers.js +48 -8
  14. package/dist/admin/field-renderers.js.map +1 -1
  15. package/dist/admin/layout.d.ts.map +1 -1
  16. package/dist/admin/layout.js +49 -2
  17. package/dist/admin/layout.js.map +1 -1
  18. package/dist/admin/list-view.d.ts +9 -0
  19. package/dist/admin/list-view.d.ts.map +1 -1
  20. package/dist/admin/list-view.js +109 -7
  21. package/dist/admin/list-view.js.map +1 -1
  22. package/dist/admin-client.js +1 -1
  23. package/dist/api/local-api.d.ts +14 -3
  24. package/dist/api/local-api.d.ts.map +1 -1
  25. package/dist/api/local-api.js +112 -7
  26. package/dist/api/local-api.js.map +1 -1
  27. package/dist/api/rest-api.d.ts +8 -1
  28. package/dist/api/rest-api.d.ts.map +1 -1
  29. package/dist/api/rest-api.js +76 -12
  30. package/dist/api/rest-api.js.map +1 -1
  31. package/dist/auth/index.d.ts +1 -1
  32. package/dist/auth/index.d.ts.map +1 -1
  33. package/dist/auth/index.js +1 -1
  34. package/dist/auth/index.js.map +1 -1
  35. package/dist/auth/session.d.ts +7 -1
  36. package/dist/auth/session.d.ts.map +1 -1
  37. package/dist/auth/session.js +10 -3
  38. package/dist/auth/session.js.map +1 -1
  39. package/dist/config/cms-config.d.ts +12 -0
  40. package/dist/config/cms-config.d.ts.map +1 -1
  41. package/dist/config/cms-config.js +13 -3
  42. package/dist/config/cms-config.js.map +1 -1
  43. package/dist/config/index.d.ts +1 -1
  44. package/dist/config/index.d.ts.map +1 -1
  45. package/dist/db/migration-generator.d.ts +3 -3
  46. package/dist/db/migration-generator.d.ts.map +1 -1
  47. package/dist/db/migration-generator.js +27 -9
  48. package/dist/db/migration-generator.js.map +1 -1
  49. package/dist/db/query-builder.d.ts +3 -1
  50. package/dist/db/query-builder.d.ts.map +1 -1
  51. package/dist/db/query-builder.js +54 -14
  52. package/dist/db/query-builder.js.map +1 -1
  53. package/dist/db/sql-sanitize.d.ts.map +1 -1
  54. package/dist/db/sql-sanitize.js +5 -1
  55. package/dist/db/sql-sanitize.js.map +1 -1
  56. package/dist/hooks/hook-types.d.ts +5 -1
  57. package/dist/hooks/hook-types.d.ts.map +1 -1
  58. package/dist/index.d.ts +9 -7
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +5 -4
  61. package/dist/index.js.map +1 -1
  62. package/dist/media/image-processor.d.ts +17 -0
  63. package/dist/media/image-processor.d.ts.map +1 -0
  64. package/dist/media/image-processor.js +90 -0
  65. package/dist/media/image-processor.js.map +1 -0
  66. package/dist/media/index.d.ts +8 -3
  67. package/dist/media/index.d.ts.map +1 -1
  68. package/dist/media/index.js +4 -2
  69. package/dist/media/index.js.map +1 -1
  70. package/dist/media/media-config.d.ts +14 -0
  71. package/dist/media/media-config.d.ts.map +1 -1
  72. package/dist/media/media-config.js +8 -1
  73. package/dist/media/media-config.js.map +1 -1
  74. package/dist/media/serve-handler.d.ts +3 -1
  75. package/dist/media/serve-handler.d.ts.map +1 -1
  76. package/dist/media/serve-handler.js +39 -17
  77. package/dist/media/serve-handler.js.map +1 -1
  78. package/dist/media/storage-adapter.d.ts +10 -0
  79. package/dist/media/storage-adapter.d.ts.map +1 -0
  80. package/dist/media/storage-adapter.js +46 -0
  81. package/dist/media/storage-adapter.js.map +1 -0
  82. package/dist/media/upload-handler.d.ts +13 -1
  83. package/dist/media/upload-handler.d.ts.map +1 -1
  84. package/dist/media/upload-handler.js +140 -30
  85. package/dist/media/upload-handler.js.map +1 -1
  86. package/dist/scheduler.d.ts +7 -0
  87. package/dist/scheduler.d.ts.map +1 -0
  88. package/dist/scheduler.js +14 -0
  89. package/dist/scheduler.js.map +1 -0
  90. package/dist/schema/collection.d.ts +9 -1
  91. package/dist/schema/collection.d.ts.map +1 -1
  92. package/dist/schema/collection.js.map +1 -1
  93. package/dist/schema/index.d.ts +2 -2
  94. package/dist/schema/index.d.ts.map +1 -1
  95. package/dist/schema/index.js +1 -1
  96. package/dist/schema/index.js.map +1 -1
  97. package/dist/schema/types.d.ts +5 -0
  98. package/dist/schema/types.d.ts.map +1 -1
  99. package/dist/schema/types.js +4 -0
  100. package/dist/schema/types.js.map +1 -1
  101. package/dist/validation/index.d.ts +1 -1
  102. package/dist/validation/index.d.ts.map +1 -1
  103. package/dist/validation/index.js +1 -1
  104. package/dist/validation/index.js.map +1 -1
  105. package/dist/validation/zod-generator.d.ts +4 -0
  106. package/dist/validation/zod-generator.d.ts.map +1 -1
  107. package/dist/validation/zod-generator.js +12 -1
  108. package/dist/validation/zod-generator.js.map +1 -1
  109. package/package.json +5 -4
@@ -0,0 +1,90 @@
1
+ import sharp from 'sharp';
2
+ import { ok, err, ResultAsync } from 'neverthrow';
3
+ import { CmsErrorCode } from '../schema/types.js';
4
+ function toSharpError(e) {
5
+ return {
6
+ code: CmsErrorCode.INTERNAL,
7
+ message: e instanceof Error ? e.message : 'Image processing failed'
8
+ };
9
+ }
10
+ async function applyFocalPointCrop(pipeline, size, focalPoint) {
11
+ const metaResult = await ResultAsync.fromPromise(pipeline.metadata(), toSharpError);
12
+ if (metaResult.isErr())
13
+ return err(metaResult.error);
14
+ const meta = metaResult.value;
15
+ const srcW = meta.width ?? 0;
16
+ const srcH = meta.height ?? 0;
17
+ if (srcW === 0 || srcH === 0) {
18
+ return ok(pipeline.resize({ width: size.width, height: size.height, fit: 'cover' }));
19
+ }
20
+ const targetW = size.width;
21
+ const targetH = size.height;
22
+ const scale = Math.max(targetW / srcW, targetH / srcH);
23
+ const scaledW = Math.round(srcW * scale);
24
+ const scaledH = Math.round(srcH * scale);
25
+ const focalX = Math.round(focalPoint.x * scaledW);
26
+ const focalY = Math.round(focalPoint.y * scaledH);
27
+ const left = Math.min(Math.max(focalX - Math.round(targetW / 2), 0), scaledW - targetW);
28
+ const top = Math.min(Math.max(focalY - Math.round(targetH / 2), 0), scaledH - targetH);
29
+ return ok(pipeline
30
+ .resize({ width: scaledW, height: scaledH, fit: 'fill' })
31
+ .extract({ left, top, width: targetW, height: targetH }));
32
+ }
33
+ async function buildPipeline(inputBuffer, size, focalPoint) {
34
+ const fit = size.fit ?? 'cover';
35
+ if (focalPoint && fit === 'cover') {
36
+ return applyFocalPointCrop(sharp(inputBuffer), size, focalPoint);
37
+ }
38
+ return ok(sharp(inputBuffer).resize({
39
+ width: size.width,
40
+ height: size.height,
41
+ fit
42
+ }));
43
+ }
44
+ function toProcessedImage(name, outputBuffer, info) {
45
+ return {
46
+ name,
47
+ buffer: outputBuffer,
48
+ width: info.width,
49
+ height: info.height,
50
+ filesize: outputBuffer.length,
51
+ mimeType: `image/${info.format}`
52
+ };
53
+ }
54
+ async function processSize(inputBuffer, size, focalPoint) {
55
+ const pipelineResult = await buildPipeline(inputBuffer, size, focalPoint);
56
+ if (pipelineResult.isErr())
57
+ return err(pipelineResult.error);
58
+ const bufferResult = await ResultAsync.fromPromise(pipelineResult.value.toBuffer({ resolveWithObject: true }), toSharpError);
59
+ if (bufferResult.isErr())
60
+ return err(bufferResult.error);
61
+ const { data, info } = bufferResult.value;
62
+ return ok(toProcessedImage(size.name, data, info));
63
+ }
64
+ async function processWebpVariant(inputBuffer, size, focalPoint) {
65
+ const pipelineResult = await buildPipeline(inputBuffer, size, focalPoint);
66
+ if (pipelineResult.isErr())
67
+ return err(pipelineResult.error);
68
+ const webpResult = await ResultAsync.fromPromise(pipelineResult.value.webp({ quality: 80 }).toBuffer({ resolveWithObject: true }), toSharpError);
69
+ if (webpResult.isErr())
70
+ return err(webpResult.error);
71
+ const { data, info } = webpResult.value;
72
+ return ok(toProcessedImage(`${size.name}-webp`, data, info));
73
+ }
74
+ export async function processImageSizes(inputBuffer, sizes, formats = [], focalPoint) {
75
+ const results = [];
76
+ for (const size of sizes) {
77
+ const sizeResult = await processSize(inputBuffer, size, focalPoint);
78
+ if (sizeResult.isErr())
79
+ return err(sizeResult.error);
80
+ results.push(sizeResult.value);
81
+ if (formats.includes('webp')) {
82
+ const webpResult = await processWebpVariant(inputBuffer, size, focalPoint);
83
+ if (webpResult.isErr())
84
+ return err(webpResult.error);
85
+ results.push(webpResult.value);
86
+ }
87
+ }
88
+ return ok(results);
89
+ }
90
+ //# sourceMappingURL=image-processor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-processor.js","sourceRoot":"","sources":["../../src/media/image-processor.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,10 @@
1
- export { isUploadEnabled, getMediaFields, getMimeType } from './media-config.js';
2
- export { createServeHandler } from './serve-handler.js';
1
+ export { isUploadEnabled, getMediaFields, getMimeType, getUploadConfig } from './media-config.js';
2
+ export type { ImageSize, UploadConfig } from './media-config.js';
3
+ export { createServeHandler, buildMediaUrl } from './serve-handler.js';
3
4
  export { createUploadHandler } from './upload-handler.js';
4
- export type { UploadResult } from './upload-handler.js';
5
+ export type { UploadResult, SizeMetadata } from './upload-handler.js';
6
+ export { createLocalStorage } from './storage-adapter.js';
7
+ export type { StorageAdapter } from './storage-adapter.js';
8
+ export { processImageSizes } from './image-processor.js';
9
+ export type { ProcessedImage, FocalPoint } from './image-processor.js';
5
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACjG,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA"}
@@ -1,4 +1,6 @@
1
- export { isUploadEnabled, getMediaFields, getMimeType } from './media-config.js';
2
- export { createServeHandler } from './serve-handler.js';
1
+ export { isUploadEnabled, getMediaFields, getMimeType, getUploadConfig } from './media-config.js';
2
+ export { createServeHandler, buildMediaUrl } from './serve-handler.js';
3
3
  export { createUploadHandler } from './upload-handler.js';
4
+ export { createLocalStorage } from './storage-adapter.js';
5
+ export { processImageSizes } from './image-processor.js';
4
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEjG,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAEzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA"}
@@ -1,6 +1,20 @@
1
1
  import type { CollectionConfig } from '../schema/collection.js';
2
2
  import type { FieldConfig } from '../schema/field-types.js';
3
+ export interface ImageSize {
4
+ readonly name: string;
5
+ readonly width: number;
6
+ readonly height: number;
7
+ readonly fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' | undefined;
8
+ }
9
+ export interface UploadConfig {
10
+ readonly mimeTypes?: readonly string[] | undefined;
11
+ readonly maxFileSize?: number | undefined;
12
+ readonly imageSizes?: readonly ImageSize[] | undefined;
13
+ readonly focalPoint?: boolean | undefined;
14
+ readonly formats?: readonly string[] | undefined;
15
+ }
3
16
  export declare function isUploadEnabled(collection: CollectionConfig): boolean;
17
+ export declare function getUploadConfig(collection: CollectionConfig): UploadConfig | null;
4
18
  export declare function getMediaFields(): readonly FieldConfig[];
5
19
  export declare function getMimeType(filename: string): string;
6
20
  //# sourceMappingURL=media-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"media-config.d.ts","sourceRoot":"","sources":["../../src/media/media-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE3D,wBAAgB,eAAe,CAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAEtE;AAED,wBAAgB,cAAc,IAAK,SAAS,WAAW,EAAE,CAQxD;AAuBD,wBAAgB,WAAW,CAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD"}
1
+ {"version":3,"file":"media-config.d.ts","sourceRoot":"","sources":["../../src/media/media-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE3D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAA;CAC/E;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IAClD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzC,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,CAAA;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CACjD;AAED,wBAAgB,eAAe,CAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAEtE;AAED,wBAAgB,eAAe,CAAE,UAAU,EAAE,gBAAgB,GAAG,YAAY,GAAG,IAAI,CAIlF;AAED,wBAAgB,cAAc,IAAK,SAAS,WAAW,EAAE,CAQxD;AAuBD,wBAAgB,WAAW,CAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD"}
@@ -1,5 +1,12 @@
1
1
  export function isUploadEnabled(collection) {
2
- return collection.upload === true;
2
+ return collection.upload !== undefined && collection.upload !== false;
3
+ }
4
+ export function getUploadConfig(collection) {
5
+ if (collection.upload === true)
6
+ return {};
7
+ if (collection.upload === false || collection.upload === undefined)
8
+ return null;
9
+ return collection.upload;
3
10
  }
4
11
  export function getMediaFields() {
5
12
  return [
@@ -1 +1 @@
1
- {"version":3,"file":"media-config.js","sourceRoot":"","sources":["../../src/media/media-config.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,eAAe,CAAE,UAA4B;IAC3D,OAAO,UAAU,CAAC,MAAM,KAAK,IAAI,CAAA;AACnC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;QAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;QAClD,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;QACpD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE;QACpD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;KAClC,CAAA;AACH,CAAC;AAED,MAAM,QAAQ,GAA2B;IACvC,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,iBAAiB;IACtB,IAAI,EAAE,kBAAkB;IACxB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,WAAW;IACjB,GAAG,EAAE,UAAU;IACf,EAAE,EAAE,wBAAwB;IAC5B,GAAG,EAAE,iBAAiB;CACvB,CAAA;AAED,MAAM,UAAU,WAAW,CAAE,QAAgB;IAC3C,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IAC1D,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAA;AACpD,CAAC"}
1
+ {"version":3,"file":"media-config.js","sourceRoot":"","sources":["../../src/media/media-config.ts"],"names":[],"mappings":"AAkBA,MAAM,UAAU,eAAe,CAAE,UAA4B;IAC3D,OAAO,UAAU,CAAC,MAAM,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,KAAK,KAAK,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,eAAe,CAAE,UAA4B;IAC3D,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,CAAA;IACzC,IAAI,UAAU,CAAC,MAAM,KAAK,KAAK,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAC/E,OAAO,UAAU,CAAC,MAAM,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;QAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;QAClD,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;QACpD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE;QACpD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;KAClC,CAAA;AACH,CAAC;AAED,MAAM,QAAQ,GAA2B;IACvC,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,iBAAiB;IACtB,IAAI,EAAE,kBAAkB;IACxB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,WAAW;IACjB,GAAG,EAAE,UAAU;IACf,EAAE,EAAE,wBAAwB;IAC5B,GAAG,EAAE,iBAAiB;CACvB,CAAA;AAED,MAAM,UAAU,WAAW,CAAE,QAAgB;IAC3C,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IAC1D,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAA;AACpD,CAAC"}
@@ -1,3 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- export declare function createServeHandler(uploadDir: string): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
2
+ import type { StorageAdapter } from './storage-adapter.js';
3
+ export declare function createServeHandler(uploadDir: string, storage?: StorageAdapter): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
4
+ export declare function buildMediaUrl(filename: string, sizeName?: string): string;
3
5
  //# sourceMappingURL=serve-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve-handler.d.ts","sourceRoot":"","sources":["../../src/media/serve-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAUhE,wBAAgB,kBAAkB,CAAE,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAgDnH"}
1
+ {"version":3,"file":"serve-handler.d.ts","sourceRoot":"","sources":["../../src/media/serve-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAOhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAkB1D,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,cAAc,GACvB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CA+C9D;AAED,wBAAgB,aAAa,CAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAW1E"}
@@ -4,25 +4,43 @@ import { ResultAsync } from 'neverthrow';
4
4
  import { CmsErrorCode } from '../schema/types.js';
5
5
  import { getMimeType } from './media-config.js';
6
6
  const SAFE_FILENAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
7
- export function createServeHandler(uploadDir) {
7
+ function sendFile(res, filename, data) {
8
+ res.writeHead(200, {
9
+ 'Content-Type': getMimeType(filename),
10
+ 'Content-Length': data.length,
11
+ 'Cache-Control': 'public, max-age=31536000, immutable'
12
+ });
13
+ res.end(data);
14
+ }
15
+ function sendError(res, status, error) {
16
+ res.writeHead(status, { 'Content-Type': 'application/json' });
17
+ res.end(JSON.stringify({ error }));
18
+ }
19
+ export function createServeHandler(uploadDir, storage) {
8
20
  const resolvedUploadDir = resolve(uploadDir);
9
21
  return async (req, res) => {
10
22
  const rawFilename = req.url?.split('/').pop();
11
23
  if (!rawFilename) {
12
- res.writeHead(400, { 'Content-Type': 'application/json' });
13
- res.end(JSON.stringify({ error: 'Missing filename' }));
24
+ sendError(res, 400, 'Missing filename');
14
25
  return;
15
26
  }
16
27
  const filename = basename(rawFilename);
17
28
  if (!SAFE_FILENAME_RE.test(filename)) {
18
- res.writeHead(400, { 'Content-Type': 'application/json' });
19
- res.end(JSON.stringify({ error: 'Invalid filename' }));
29
+ sendError(res, 400, 'Invalid filename');
30
+ return;
31
+ }
32
+ if (storage) {
33
+ const result = await storage.read(filename);
34
+ if (result.isErr()) {
35
+ sendError(res, 404, 'File not found');
36
+ return;
37
+ }
38
+ sendFile(res, filename, result.value);
20
39
  return;
21
40
  }
22
41
  const filePath = resolve(join(resolvedUploadDir, filename));
23
42
  if (!filePath.startsWith(resolvedUploadDir)) {
24
- res.writeHead(403, { 'Content-Type': 'application/json' });
25
- res.end(JSON.stringify({ error: 'Forbidden' }));
43
+ sendError(res, 403, 'Forbidden');
26
44
  return;
27
45
  }
28
46
  const result = await ResultAsync.fromPromise(readFile(filePath), (e) => ({
@@ -30,18 +48,22 @@ export function createServeHandler(uploadDir) {
30
48
  message: e instanceof Error ? e.message : 'File read failed'
31
49
  }));
32
50
  if (result.isErr()) {
33
- res.writeHead(404, { 'Content-Type': 'application/json' });
34
- res.end(JSON.stringify({ error: 'File not found' }));
51
+ sendError(res, 404, 'File not found');
35
52
  return;
36
53
  }
37
- const data = result.value;
38
- const contentType = getMimeType(filename);
39
- res.writeHead(200, {
40
- 'Content-Type': contentType,
41
- 'Content-Length': data.length,
42
- 'Cache-Control': 'public, max-age=31536000, immutable'
43
- });
44
- res.end(data);
54
+ sendFile(res, filename, result.value);
45
55
  };
46
56
  }
57
+ export function buildMediaUrl(filename, sizeName) {
58
+ const base = basename(filename);
59
+ if (!sizeName)
60
+ return `/media/${encodeURIComponent(base)}`;
61
+ const dotIndex = base.lastIndexOf('.');
62
+ if (dotIndex === -1) {
63
+ return `/media/${encodeURIComponent(`${base}-${sizeName}`)}`;
64
+ }
65
+ const nameWithoutExt = base.slice(0, dotIndex);
66
+ const ext = base.slice(dotIndex + 1);
67
+ return `/media/${encodeURIComponent(`${nameWithoutExt}-${sizeName}.${ext}`)}`;
68
+ }
47
69
  //# sourceMappingURL=serve-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve-handler.js","sourceRoot":"","sources":["../../src/media/serve-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serve-handler.js","sourceRoot":"","sources":["../../src/media/serve-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { ResultAsync } from 'neverthrow';
2
+ import type { CmsError } from '../schema/types.js';
3
+ export interface StorageAdapter {
4
+ readonly write: (filename: string, data: Buffer) => ResultAsync<string, CmsError>;
5
+ readonly read: (filename: string) => ResultAsync<Buffer, CmsError>;
6
+ readonly remove: (filename: string) => ResultAsync<void, CmsError>;
7
+ readonly url: (filename: string) => string;
8
+ }
9
+ export declare function createLocalStorage(uploadDir: string): StorageAdapter;
10
+ //# sourceMappingURL=storage-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-adapter.d.ts","sourceRoot":"","sources":["../../src/media/storage-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAY,MAAM,YAAY,CAAA;AAElD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAIlD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACjF,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAClE,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IAClE,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;CAC3C;AAED,wBAAgB,kBAAkB,CAAE,SAAS,EAAE,MAAM,GAAG,cAAc,CAqDrE"}
@@ -0,0 +1,46 @@
1
+ import { ResultAsync, errAsync } from 'neverthrow';
2
+ import { CmsErrorCode } from '../schema/types.js';
3
+ import { writeFile, readFile, unlink } from 'node:fs/promises';
4
+ import { join, resolve, basename } from 'node:path';
5
+ export function createLocalStorage(uploadDir) {
6
+ const resolvedDir = resolve(uploadDir);
7
+ return {
8
+ write(filename, data) {
9
+ const safeName = basename(filename);
10
+ const filePath = resolve(join(resolvedDir, safeName));
11
+ if (!filePath.startsWith(resolvedDir)) {
12
+ return errAsync({ code: CmsErrorCode.FORBIDDEN, message: 'Path traversal rejected' });
13
+ }
14
+ return ResultAsync.fromPromise(writeFile(filePath, data).then(() => safeName), (e) => ({
15
+ code: CmsErrorCode.INTERNAL,
16
+ message: e instanceof Error ? e.message : 'File write failed'
17
+ }));
18
+ },
19
+ read(filename) {
20
+ const safeName = basename(filename);
21
+ const filePath = resolve(join(resolvedDir, safeName));
22
+ if (!filePath.startsWith(resolvedDir)) {
23
+ return errAsync({ code: CmsErrorCode.FORBIDDEN, message: 'Path traversal rejected' });
24
+ }
25
+ return ResultAsync.fromPromise(readFile(filePath), (e) => ({
26
+ code: CmsErrorCode.NOT_FOUND,
27
+ message: e instanceof Error ? e.message : 'File read failed'
28
+ }));
29
+ },
30
+ remove(filename) {
31
+ const safeName = basename(filename);
32
+ const filePath = resolve(join(resolvedDir, safeName));
33
+ if (!filePath.startsWith(resolvedDir)) {
34
+ return errAsync({ code: CmsErrorCode.FORBIDDEN, message: 'Path traversal rejected' });
35
+ }
36
+ return ResultAsync.fromPromise(unlink(filePath), (e) => ({
37
+ code: CmsErrorCode.NOT_FOUND,
38
+ message: e instanceof Error ? e.message : 'File delete failed'
39
+ }));
40
+ },
41
+ url(filename) {
42
+ return `/media/${encodeURIComponent(basename(filename))}`;
43
+ }
44
+ };
45
+ }
46
+ //# sourceMappingURL=storage-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-adapter.js","sourceRoot":"","sources":["../../src/media/storage-adapter.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,21 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { UploadConfig } from './media-config.js';
3
+ import type { StorageAdapter } from './storage-adapter.js';
4
+ export interface SizeMetadata {
5
+ readonly filename: string;
6
+ readonly width: number;
7
+ readonly height: number;
8
+ readonly filesize: number;
9
+ readonly mimeType: string;
10
+ }
2
11
  export interface UploadResult {
3
12
  readonly filename: string;
4
13
  readonly storedPath: string;
5
14
  readonly mimeType: string;
6
15
  readonly filesize: number;
16
+ readonly focalX?: number | undefined;
17
+ readonly focalY?: number | undefined;
18
+ readonly sizes?: Record<string, SizeMetadata> | undefined;
7
19
  }
8
- export declare function createUploadHandler(uploadDir: string): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
20
+ export declare function createUploadHandler(uploadDir: string, storage?: StorageAdapter, uploadConfig?: UploadConfig): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
9
21
  //# sourceMappingURL=upload-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload-handler.d.ts","sourceRoot":"","sources":["../../src/media/upload-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAgBhE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,mBAAmB,CAAE,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAyDpH"}
1
+ {"version":3,"file":"upload-handler.d.ts","sourceRoot":"","sources":["../../src/media/upload-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAKhE,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AA0B1D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,SAAS,CAAA;CAC1D;AAoID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,cAAc,EACxB,YAAY,CAAC,EAAE,YAAY,GAC1B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAkE9D"}
@@ -1,63 +1,173 @@
1
- import { ResultAsync } from 'neverthrow';
1
+ import { ResultAsync, okAsync } from 'neverthrow';
2
2
  import { CmsErrorCode } from '../schema/types.js';
3
3
  import { writeFile } from 'node:fs/promises';
4
4
  import { join, resolve, basename } from 'node:path';
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { getMimeType } from './media-config.js';
7
+ import { processImageSizes } from './image-processor.js';
7
8
  import { readRawBody } from '../api/read-body.js';
8
9
  const MAX_UPLOAD_BYTES = 10_485_760;
9
10
  const SAFE_FILENAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
11
+ const IMAGE_MIMES = new Set([
12
+ 'image/jpeg',
13
+ 'image/png',
14
+ 'image/webp',
15
+ 'image/avif',
16
+ 'image/gif'
17
+ ]);
18
+ function isImageFile(mimeType) {
19
+ return IMAGE_MIMES.has(mimeType);
20
+ }
10
21
  function generateStoredName(originalName) {
11
22
  const ext = originalName.split('.').pop() ?? '';
12
23
  const prefix = randomBytes(16).toString('hex');
13
24
  return ext ? `${prefix}.${ext}` : prefix;
14
25
  }
15
- export function createUploadHandler(uploadDir) {
26
+ function parseFocalPoint(req) {
27
+ const rawX = req.headers['x-focal-x'];
28
+ const rawY = req.headers['x-focal-y'];
29
+ const xStr = Array.isArray(rawX) ? rawX[0] : rawX;
30
+ const yStr = Array.isArray(rawY) ? rawY[0] : rawY;
31
+ if (xStr === undefined || yStr === undefined)
32
+ return undefined;
33
+ const x = Number(xStr);
34
+ const y = Number(yStr);
35
+ if (Number.isNaN(x) || Number.isNaN(y))
36
+ return undefined;
37
+ if (x < 0 || x > 1 || y < 0 || y > 1)
38
+ return undefined;
39
+ return { x, y };
40
+ }
41
+ function buildSizeMetadata(processed, originalExt) {
42
+ const sizes = {};
43
+ for (const img of processed) {
44
+ const ext = img.mimeType.split('/').pop() ?? originalExt;
45
+ sizes[img.name] = {
46
+ filename: `${img.name}.${ext}`,
47
+ width: img.width,
48
+ height: img.height,
49
+ filesize: img.filesize,
50
+ mimeType: img.mimeType
51
+ };
52
+ }
53
+ return sizes;
54
+ }
55
+ function variantFilename(storedName, sizeName, ext) {
56
+ const base = storedName.split('.').slice(0, -1).join('.');
57
+ return `${base}-${sizeName}.${ext}`;
58
+ }
59
+ function writeVariants(storage, storedName, processed) {
60
+ let chain = okAsync(undefined);
61
+ for (const img of processed) {
62
+ const ext = img.mimeType.split('/').pop() ?? 'bin';
63
+ const name = variantFilename(storedName, img.name, ext);
64
+ chain = chain.andThen(() => storage.write(name, img.buffer).map(() => undefined));
65
+ }
66
+ return chain;
67
+ }
68
+ function sendJson(res, status, data) {
69
+ const body = JSON.stringify(data);
70
+ res.writeHead(status, {
71
+ 'Content-Type': 'application/json; charset=utf-8',
72
+ 'Content-Length': Buffer.byteLength(body)
73
+ });
74
+ res.end(body);
75
+ }
76
+ function sendError(res, status, message) {
77
+ res.writeHead(status, { 'Content-Type': 'application/json' });
78
+ res.end(JSON.stringify({ error: message }));
79
+ }
80
+ function getImageProcessingContext(storage, mimeType, uploadConfig) {
81
+ if (!storage || !isImageFile(mimeType))
82
+ return null;
83
+ if (!uploadConfig?.imageSizes || uploadConfig.imageSizes.length === 0)
84
+ return null;
85
+ return {
86
+ storage,
87
+ imageSizes: uploadConfig.imageSizes,
88
+ formats: uploadConfig.formats ?? []
89
+ };
90
+ }
91
+ async function writeMainFile(storage, resolvedDir, storedName, data) {
92
+ if (storage) {
93
+ const writeResult = await storage.write(storedName, data);
94
+ return writeResult.isErr() ? writeResult.error : null;
95
+ }
96
+ const storedPath = resolve(join(resolvedDir, storedName));
97
+ if (!storedPath.startsWith(resolvedDir)) {
98
+ return { code: CmsErrorCode.FORBIDDEN, message: 'Forbidden' };
99
+ }
100
+ const writeResult = await ResultAsync.fromPromise(writeFile(storedPath, data), (e) => ({
101
+ code: CmsErrorCode.INTERNAL,
102
+ message: e instanceof Error ? e.message : 'File write failed'
103
+ }));
104
+ return writeResult.isErr() ? writeResult.error : null;
105
+ }
106
+ function buildUploadResult(originalName, storedName, mimeType, filesize, focalX, focalY, sizes) {
107
+ return {
108
+ filename: originalName,
109
+ storedPath: storedName,
110
+ mimeType,
111
+ filesize,
112
+ ...(focalX !== undefined && focalY !== undefined ? { focalX, focalY } : {}),
113
+ ...(sizes ? { sizes } : {})
114
+ };
115
+ }
116
+ export function createUploadHandler(uploadDir, storage, uploadConfig) {
16
117
  const resolvedDir = resolve(uploadDir);
17
118
  return async (req, res) => {
18
119
  const filenameHeader = req.headers['x-filename'];
19
120
  const rawName = Array.isArray(filenameHeader) ? filenameHeader[0] ?? 'upload' : filenameHeader ?? 'upload';
20
121
  const originalName = basename(rawName);
21
122
  if (!SAFE_FILENAME_RE.test(originalName)) {
22
- res.writeHead(400, { 'Content-Type': 'application/json' });
23
- res.end(JSON.stringify({ error: 'Invalid filename' }));
123
+ sendError(res, 400, 'Invalid filename');
24
124
  return;
25
125
  }
26
126
  const bodyResult = await readRawBody(req, MAX_UPLOAD_BYTES);
27
127
  if (bodyResult.isErr()) {
28
- res.writeHead(400, { 'Content-Type': 'application/json' });
29
- res.end(JSON.stringify({ error: bodyResult.error.message }));
128
+ sendError(res, 400, bodyResult.error.message);
30
129
  return;
31
130
  }
32
131
  const data = bodyResult.value;
33
132
  const storedName = generateStoredName(originalName);
34
- const storedPath = resolve(join(resolvedDir, storedName));
35
- if (!storedPath.startsWith(resolvedDir)) {
36
- res.writeHead(403, { 'Content-Type': 'application/json' });
37
- res.end(JSON.stringify({ error: 'Forbidden' }));
133
+ const mimeType = getMimeType(originalName);
134
+ const writeError = await writeMainFile(storage, resolvedDir, storedName, data);
135
+ if (writeError) {
136
+ const status = writeError.code === CmsErrorCode.FORBIDDEN ? 403 : 500;
137
+ sendError(res, status, writeError.message);
38
138
  return;
39
139
  }
40
- const writeResult = await ResultAsync.fromPromise(writeFile(storedPath, data), (e) => ({
41
- code: CmsErrorCode.INTERNAL,
42
- message: e instanceof Error ? e.message : 'File write failed'
43
- }));
44
- if (writeResult.isErr()) {
45
- res.writeHead(500, { 'Content-Type': 'application/json' });
46
- res.end(JSON.stringify({ error: writeResult.error.message }));
47
- return;
140
+ let focalX;
141
+ let focalY;
142
+ let sizes;
143
+ if (isImageFile(mimeType) && uploadConfig?.focalPoint) {
144
+ const fp = parseFocalPoint(req);
145
+ if (fp) {
146
+ focalX = fp.x;
147
+ focalY = fp.y;
148
+ }
48
149
  }
49
- const result = {
50
- filename: originalName,
51
- storedPath: storedName,
52
- mimeType: getMimeType(originalName),
53
- filesize: data.length
54
- };
55
- const body = JSON.stringify(result);
56
- res.writeHead(201, {
57
- 'Content-Type': 'application/json; charset=utf-8',
58
- 'Content-Length': Buffer.byteLength(body)
59
- });
60
- res.end(body);
150
+ const imageCtx = getImageProcessingContext(storage, mimeType, uploadConfig);
151
+ if (imageCtx) {
152
+ const focalPoint = (focalX !== undefined && focalY !== undefined)
153
+ ? { x: focalX, y: focalY }
154
+ : undefined;
155
+ const processResult = await processImageSizes(data, imageCtx.imageSizes, imageCtx.formats, focalPoint);
156
+ if (processResult.isErr()) {
157
+ sendError(res, 500, processResult.error.message);
158
+ return;
159
+ }
160
+ const processed = processResult.value;
161
+ const originalExt = originalName.split('.').pop() ?? '';
162
+ sizes = buildSizeMetadata(processed, originalExt);
163
+ const variantResult = await writeVariants(imageCtx.storage, storedName, processed);
164
+ if (variantResult.isErr()) {
165
+ sendError(res, 500, variantResult.error.message);
166
+ return;
167
+ }
168
+ }
169
+ const result = buildUploadResult(originalName, storedName, mimeType, data.length, focalX, focalY, sizes);
170
+ sendJson(res, 201, result);
61
171
  };
62
172
  }
63
173
  //# sourceMappingURL=upload-handler.js.map